From 0de2b94af60ac94f2844f5f2f6195264eacbbc34 Mon Sep 17 00:00:00 2001 From: nirajCITZ <94716060+nirajCITZ@users.noreply.github.com> Date: Mon, 13 Feb 2023 12:19:22 -0800 Subject: [PATCH 1/8] Cypress delete resource (#742) * upd cypress ghaction and issuer owner * fix for failed testing * allow provider in user batch load * no message * fix validation error for jwt keycloak * Update Cypress version 12.0.4 and update the folder structure as per cypress migration guide * Update cypress test to sign out and clear cookies after execution * update Dockerfile for e2e * Update dockerfile * Update npm dependency in Dockerfile * update npm command * Update dockerfile for cypress e2e * Update Cypress Code coverage version * update npm command in e2e Dockerfile * Update aps-cypress-e2e.yaml Disable code-coverage step in workflow * Update Docker file and package.json * update oidc-audience-mapper to policy * Updated cypress test for delete resources and added hook to sign out and clear cookies * Update failing cypress tests --------- Co-authored-by: ikethecoder Co-authored-by: Niraj Patel --- .github/workflows/aps-cypress-e2e.yaml | 12 +- e2e/Dockerfile | 8 +- e2e/cypress.config.ts | 63 ++++ e2e/cypress.json | 32 -- e2e/cypress/fixtures/api.json | 6 +- e2e/cypress/fixtures/apiowner.json | 2 +- e2e/cypress/fixtures/developer.json | 10 + .../manage-control/kong-plugin-config.json | 2 +- e2e/cypress/fixtures/state/regen.json | 7 +- e2e/cypress/fixtures/state/store.json | 4 +- .../test_data/authorizationProfile.json | 6 +- e2e/cypress/pageObjects/apiDirectory.ts | 1 - e2e/cypress/pageObjects/consumers.ts | 2 +- e2e/cypress/pageObjects/home.ts | 4 +- e2e/cypress/pageObjects/products.ts | 27 +- e2e/cypress/pageObjects/serviceAccounts.ts | 38 ++- e2e/cypress/plugins/index.ts | 8 +- e2e/cypress/support/auth-commands.ts | 5 +- e2e/cypress/support/{index.ts => e2e.ts} | 0 e2e/cypress/support/util-commands.ts | 7 +- ...create-api.spec.ts => 01-create-api.cy.ts} | 0 ...am-access.spec.ts => 02-team-access.cy.ts} | 0 ...ess-with-out-collecting-credentials.cy.ts} | 0 ...uest-without-collecting-credentials.cy.ts} | 7 + ...s.spec.ts => 05-collect-credentials.cy.ts} | 0 ....spec.ts => 06-approve-pending-rqst.cy.ts} | 5 + ...t-access.spec.ts => 07-grant-access.cy.ts} | 6 + ... => 08-apply-kong-api-only-consumer.cy.ts} | 0 ...9-kong-api-only-apply-rate-limiting.cy.ts} | 0 ...restriction.ts => 01-ip-restriction.cy.ts} | 0 ...ate-limiting.ts => 02-rate-limiting.cy.ts} | 0 ...pec.ts => 01-rqst-access-for-labels.cy.ts} | 0 ...e-pending-rqst-for-labels.spec copy.cy.ts} | 0 ...-labels.spec.ts => 03-filter-labels.cy.ts} | 0 ...-labels-spec.ts => 04-manage-labels.cy.ts} | 0 ...ss.ts => 01-client-cred-team-access.cy.ts} | 0 ... => 02-create_authorizarion_profile.cy.ts} | 10 + ...lient-cred-create-api-prod-auth-pro.cy.ts} | 4 +- ...cess-rqst.ts => 04-cids-access-rqst.cy.ts} | 0 ... => 05-cids-access-approve-api-rqst.cy.ts} | 0 ...rqst.ts => 06-jwt-genkp-access-rqst.cy.ts} | 0 ...7-jwt-genkp-access-approve-api-rqst.cy.ts} | 0 ...=> 08-jwks-url-gen-keys-access-rqst.cy.ts} | 0 ...9-jwks-url-access-approval-api-rqst.cy.ts} | 0 ...ec.ts => 01-gateway-service-details.cy.ts} | 0 ...pec.ts => 02-filter-gateway-service.cy.ts} | 0 ...s.spec.ts => 01-migrate-user-access.cy.ts} | 0 .../{01-api-key.ts => 01-api-key.cy.ts} | 0 ...entials.ts => 02-client-credentials.cy.ts} | 0 ...1-client-credential-to-kong-acl-api.cy.ts} | 0 ...2-kong-acl-api-to-client-credential.cy.ts} | 0 ...es.ts => 03-apply-multiple-services.cy.ts} | 0 ...opy.ts => 04-change-env-status copy.cy.ts} | 0 ...ts => 05-keycloak-shared-IDP-config.cy.ts} | 0 .../08-update-product-env/06-shared-idp.cy.ts | 290 ++++++++++++++++++ .../08-update-product-env/06-shared-idp.ts | 139 --------- ...create-api.spec.ts => 01-create-api.cy.ts} | 49 +-- ...resources.ts => 02-delete-resources.cy.ts} | 36 ++- ...ctivity-feed.ts => 01-activity-feed.cy.ts} | 0 ...lure.ts => 02-activity-feed-failure.cy.ts} | 5 + ...create-api.spec.ts => 01-create-api.cy.ts} | 0 ...am-access.spec.ts => 02-team-access.cy.ts} | 0 ...st-access.spec.ts => 03-rqst-access.cy.ts} | 0 ...ess-manager.ts => 04-access-manager.cy.ts} | 0 ...ce-manage.ts => 05-namespace-manage.cy.ts} | 0 ...l-issuer.ts => 06-credential-issuer.cy.ts} | 0 ...espace-view.ts => 07-namespace-view.cy.ts} | 0 ...eway-config.ts => 08-gateway-config.cy.ts} | 0 ...nt-publish.ts => 09-content-publish.cy.ts} | 0 ...1-delete-application-without-access.cy.ts} | 0 ...te-application-with-pending-request.cy.ts} | 0 ...e-application-with-approved-request.cy.ts} | 1 + ...create-api.spec.ts => 01-create-api.cy.ts} | 0 ...ode.ts => 02-namespace-preview-mode.cy.ts} | 0 ...create-api.spec.ts => 01-create-api.cy.ts} | 0 ...-organization.ts => 02-organization.cy.ts} | 6 + ...ocumentation.ts => 03-documentation.cy.ts} | 6 + ...iles.ts => 04-authorizationProfiles.cy.ts} | 5 + .../{05-products.ts => 05-products.cy.ts} | 6 + ...pi-directory.ts => 06-api-directory.cy.ts} | 6 + .../{07-namespaces.ts => 07-namespaces.cy.ts} | 6 + e2e/package-lock.json | 154 ++++++++-- e2e/package.json | 11 +- e2e/tsconfig.json | 3 +- local/cypress-jwks-url/keys.json | 2 +- local/feeder-init/idir-user.yaml | 1 + local/feeder-init/platform-authz-profile.yaml | 2 +- local/feeder-init/shared-idp-copy.yaml | 20 ++ local/feeder-init/shared-idp.yaml | 6 +- local/keycloak/master-realm.json | 4 +- src/batch/data-rules.js | 2 +- .../workflow/validate-active-environment.ts | 29 +- .../workflow/validate-environment.ts | 21 +- .../validate-active-environment.test.js | 139 +++++++++ 94 files changed, 915 insertions(+), 310 deletions(-) create mode 100644 e2e/cypress.config.ts delete mode 100644 e2e/cypress.json rename e2e/cypress/support/{index.ts => e2e.ts} (100%) rename e2e/cypress/tests/01-api-key/{01-create-api.spec.ts => 01-create-api.cy.ts} (100%) rename e2e/cypress/tests/01-api-key/{02-team-access.spec.ts => 02-team-access.cy.ts} (100%) rename e2e/cypress/tests/01-api-key/{03-request-access-with-out-collecting-credentials.ts => 03-request-access-with-out-collecting-credentials.cy.ts} (100%) rename e2e/cypress/tests/01-api-key/{04-review-request-without-collecting-credentials.ts => 04-review-request-without-collecting-credentials.cy.ts} (92%) rename e2e/cypress/tests/01-api-key/{05-collect-credentials.spec.ts => 05-collect-credentials.cy.ts} (100%) rename e2e/cypress/tests/01-api-key/{06-approve-pending-rqst.spec.ts => 06-approve-pending-rqst.cy.ts} (97%) rename e2e/cypress/tests/01-api-key/{07-grant-access.spec.ts => 07-grant-access.cy.ts} (94%) rename e2e/cypress/tests/01-api-key/{08-apply-kong-api-only-consumer.ts => 08-apply-kong-api-only-consumer.cy.ts} (100%) rename e2e/cypress/tests/01-api-key/{09-kong-api-only-apply-rate-limiting.ts => 09-kong-api-only-apply-rate-limiting.cy.ts} (100%) rename e2e/cypress/tests/02-manage-control/{01-ip-restriction.ts => 01-ip-restriction.cy.ts} (100%) rename e2e/cypress/tests/02-manage-control/{02-rate-limiting.ts => 02-rate-limiting.cy.ts} (100%) rename e2e/cypress/tests/03-manage-labels/{01-rqst-access-for-labels.spec.ts => 01-rqst-access-for-labels.cy.ts} (100%) rename e2e/cypress/tests/03-manage-labels/{02-approve-pending-rqst-for-labels.spec copy.ts => 02-approve-pending-rqst-for-labels.spec copy.cy.ts} (100%) rename e2e/cypress/tests/03-manage-labels/{03-filter-labels.spec.ts => 03-filter-labels.cy.ts} (100%) rename e2e/cypress/tests/03-manage-labels/{04-manage-labels-spec.ts => 04-manage-labels.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{01-client-cred-team-access.ts => 01-client-cred-team-access.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{02-create_authorizarion_profile.ts => 02-create_authorizarion_profile.cy.ts} (87%) rename e2e/cypress/tests/04-client-credential-flow/{03-client-cred-create-api-prod-auth-pro.ts => 03-client-cred-create-api-prod-auth-pro.cy.ts} (98%) rename e2e/cypress/tests/04-client-credential-flow/{04-cids-access-rqst.ts => 04-cids-access-rqst.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{05-cids-access-approve-api-rqst.ts => 05-cids-access-approve-api-rqst.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{06-jwt-genkp-access-rqst.ts => 06-jwt-genkp-access-rqst.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{07-jwt-genkp-access-approve-api-rqst.ts => 07-jwt-genkp-access-approve-api-rqst.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{08-jwks-url-gen-keys-access-rqst.ts => 08-jwks-url-gen-keys-access-rqst.cy.ts} (100%) rename e2e/cypress/tests/04-client-credential-flow/{09-jwks-url-access-approval-api-rqst.ts => 09-jwks-url-access-approval-api-rqst.cy.ts} (100%) rename e2e/cypress/tests/05-gateway-services/{01-gateway-service-details-spec.ts => 01-gateway-service-details.cy.ts} (100%) rename e2e/cypress/tests/05-gateway-services/{02-filter-gateway-service.spec.ts => 02-filter-gateway-service.cy.ts} (100%) rename e2e/cypress/tests/06-migrate-user/{01-migrate-user-access.spec.ts => 01-migrate-user-access.cy.ts} (100%) rename e2e/cypress/tests/07-refresh-credential/{01-api-key.ts => 01-api-key.cy.ts} (100%) rename e2e/cypress/tests/07-refresh-credential/{02-client-credentials.ts => 02-client-credentials.cy.ts} (100%) rename e2e/cypress/tests/08-update-product-env/{01-client-credential-to-kong-acl-api.ts => 01-client-credential-to-kong-acl-api.cy.ts} (100%) rename e2e/cypress/tests/08-update-product-env/{02-kong-acl-api-to-client-credential.ts => 02-kong-acl-api-to-client-credential.cy.ts} (100%) rename e2e/cypress/tests/08-update-product-env/{03-apply-multiple-services.ts => 03-apply-multiple-services.cy.ts} (100%) rename e2e/cypress/tests/08-update-product-env/{04-change-env-status copy.ts => 04-change-env-status copy.cy.ts} (100%) rename e2e/cypress/tests/08-update-product-env/{05-keycloak-shared-IDP-config.ts => 05-keycloak-shared-IDP-config.cy.ts} (100%) create mode 100644 e2e/cypress/tests/08-update-product-env/06-shared-idp.cy.ts delete mode 100644 e2e/cypress/tests/08-update-product-env/06-shared-idp.ts rename e2e/cypress/tests/09-clear-resources/{01-create-api.spec.ts => 01-create-api.cy.ts} (67%) rename e2e/cypress/tests/09-clear-resources/{02-delete-resources.ts => 02-delete-resources.cy.ts} (70%) rename e2e/cypress/tests/10-activity-feed/{01-activity-feed.ts => 01-activity-feed.cy.ts} (100%) rename e2e/cypress/tests/10-activity-feed/{02-activity-feed-failure.ts => 02-activity-feed-failure.cy.ts} (97%) rename e2e/cypress/tests/11-access-permission/{01-create-api.spec.ts => 01-create-api.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{02-team-access.spec.ts => 02-team-access.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{03-rqst-access.spec.ts => 03-rqst-access.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{04-access-manager.ts => 04-access-manager.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{05-namespace-manage.ts => 05-namespace-manage.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{06-credential-issuer.ts => 06-credential-issuer.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{07-namespace-view.ts => 07-namespace-view.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{08-gateway-config.ts => 08-gateway-config.cy.ts} (100%) rename e2e/cypress/tests/11-access-permission/{09-content-publish.ts => 09-content-publish.cy.ts} (100%) rename e2e/cypress/tests/12-delete-application/{01-delete-application-without-access.ts => 01-delete-application-without-access.cy.ts} (100%) rename e2e/cypress/tests/12-delete-application/{02-delete-application-with-pending-request.ts => 02-delete-application-with-pending-request.cy.ts} (100%) rename e2e/cypress/tests/12-delete-application/{03-delete-application-with-approved-request.ts => 03-delete-application-with-approved-request.cy.ts} (99%) rename e2e/cypress/tests/13-namespace-preview-mode/{01-create-api.spec.ts => 01-create-api.cy.ts} (100%) rename e2e/cypress/tests/13-namespace-preview-mode/{02-namespace-preview-mode.ts => 02-namespace-preview-mode.cy.ts} (100%) rename e2e/cypress/tests/14-aps-api/{01-create-api.spec.ts => 01-create-api.cy.ts} (100%) rename e2e/cypress/tests/14-aps-api/{02-organization.ts => 02-organization.cy.ts} (98%) rename e2e/cypress/tests/14-aps-api/{03-documentation.ts => 03-documentation.cy.ts} (98%) rename e2e/cypress/tests/14-aps-api/{04-authorizationProfiles.ts => 04-authorizationProfiles.cy.ts} (97%) rename e2e/cypress/tests/14-aps-api/{05-products.ts => 05-products.cy.ts} (97%) rename e2e/cypress/tests/14-aps-api/{06-api-directory.ts => 06-api-directory.cy.ts} (98%) rename e2e/cypress/tests/14-aps-api/{07-namespaces.ts => 07-namespaces.cy.ts} (98%) create mode 100644 local/feeder-init/shared-idp-copy.yaml diff --git a/.github/workflows/aps-cypress-e2e.yaml b/.github/workflows/aps-cypress-e2e.yaml index 1394a2c25..ed3e232ed 100644 --- a/.github/workflows/aps-cypress-e2e.yaml +++ b/.github/workflows/aps-cypress-e2e.yaml @@ -2,7 +2,7 @@ name: Build and Deploy Cypress and Execute Tests on: push: - branches: ['test'] + branches: ['test', 'cypress/*'] env: DASHBOARD_PROJECT_ID: ${{ secrets.CY_DASHBOARD_PRJ_ID }} @@ -59,16 +59,6 @@ jobs: name: test-results path: ${{ github.workspace }}/e2e/results/report - - name: Upload E2E Code Coverage Report - uses: actions/upload-artifact@v2 - with: - name: code-coverage - path: ${{ github.workspace }}/e2e/coverage - - - name: Format LCOV Report - run: | - sudo sed -i -r 's/\.*\/app\/nextapp\///g' ./e2e/coverage/lcov.info - - name: SonarCloud Scan uses: sonarsource/sonarcloud-github-action@master with: diff --git a/e2e/Dockerfile b/e2e/Dockerfile index 9241bab0e..46e1ec3bc 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -1,16 +1,16 @@ -FROM cypress/included:9.2.0 +FROM cypress/included:12.4.0 WORKDIR /e2e -RUN apt-get install curl +RUN apt-get -y update; apt-get -y install curl -COPY cypress.json /e2e +COPY cypress.config.ts /e2e COPY tsconfig.json /e2e COPY package.json /e2e COPY package-lock.json /e2e COPY entrypoint.sh /tmp ADD cypress /e2e/cypress -RUN npm install --production +RUN npm install ENTRYPOINT ["npm", "run", "cy:run:html"] \ No newline at end of file diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts new file mode 100644 index 000000000..b1e027680 --- /dev/null +++ b/e2e/cypress.config.ts @@ -0,0 +1,63 @@ +import cypress, { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + // require('@cypress/code-coverage/task')(on, config) + // // include any other plugin code... + + // // It's IMPORTANT to return the config object + // // with any changed environment variables + config.specPattern=['./cypress/tests/01-*/*.ts', + './cypress/tests/02-*/*.ts', + './cypress/tests/03-*/*.ts', + './cypress/tests/04-*/*.ts', + './cypress/tests/05-*/*.ts', + './cypress/tests/06-*/*.ts', + './cypress/tests/07-*/*.ts', + './cypress/tests/08-*/*.ts', + './cypress/tests/09-*/*.ts', + './cypress/tests/10-*/*.ts', + './cypress/tests/11-*/*.ts', + './cypress/tests/12-*/*.ts', + './cypress/tests/13-*/*.ts', + './cypress/tests/14-*/*.ts' + ] + return config + }, + baseUrl: 'http://oauth2proxy.localtest.me:4180', + specPattern: 'cypress/tests/**/*.cy.ts', + screenshotOnRunFailure: true, + screenshotsFolder: 'results/report/assets', + video: false, + testIsolation: false, + watchForFileChanges: false, + reporter: 'mochawesome', + reporterOptions: { + reportDir: 'results', + html: false, + json: true, + overwrite: false, + }, + chromeWebSecurity: false, + env: { + CLIENT_ID: 'aps-portal', + CLIENT_SECRET: '8e1a17ed-cb93-4806-ac32-e303d1c86018', + OIDC_ISSUER: 'http://keycloak.localtest.me:9080', + TOKEN_URL: + 'http://keycloak.localtest.me:9080/auth/realms/master/protocol/openid-connect/token', + GWA_API_URL: 'http://gwa-api.localtest.me:2000/v2', + KONG_URL: 'http://kong.localtest.me:8000', + JWKS_URL: 'http://cypress-jwks-url.localtest.me:3500', + KONG_CONFIG_URL: 'http://kong.localtest.me:8001', + BASE_URL: 'http://oauth2proxy.localtest.me:4180', + KEYCLOAK_URL: 'http://keycloak.localtest.me:9080', + }, + retries: { + runMode: 2, + openMode: 0, + } + }, +}) diff --git a/e2e/cypress.json b/e2e/cypress.json deleted file mode 100644 index 1f3e24a22..000000000 --- a/e2e/cypress.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "baseUrl": "http://oauth2proxy.localtest.me:4180", - "integrationFolder": "cypress/tests", - "screenshotOnRunFailure": true, - "screenshotsFolder": "results/report/assets", - "video": false, - "watchForFileChanges": false, - "reporter": "mochawesome", - "reporterOptions": { - "reportDir": "results", - "html": false, - "json": true, - "overwrite": false - }, - "chromeWebSecurity": false, - "env": { - "CLIENT_ID": "aps-portal", - "CLIENT_SECRET": "8e1a17ed-cb93-4806-ac32-e303d1c86018", - "OIDC_ISSUER": "http://keycloak.localtest.me:9080", - "TOKEN_URL": "http://keycloak.localtest.me:9080/auth/realms/master/protocol/openid-connect/token", - "GWA_API_URL": "http://gwa-api.localtest.me:2000/v2", - "KONG_URL": "http://kong.localtest.me:8000", - "JWKS_URL": "http://cypress-jwks-url.localtest.me:3500", - "KONG_CONFIG_URL": "http://kong.localtest.me:8001", - "BASE_URL": "http://oauth2proxy.localtest.me:4180", - "KEYCLOAK_URL": "http://keycloak.localtest.me:9080" - }, - "retries": { - "runMode": 2, - "openMode": 0 - } -} diff --git a/e2e/cypress/fixtures/api.json b/e2e/cypress/fixtures/api.json index 282e96692..e7ae3668f 100644 --- a/e2e/cypress/fixtures/api.json +++ b/e2e/cypress/fixtures/api.json @@ -165,8 +165,8 @@ "mode": "auto", "environmentDetails": [ { - "environment": "prod", - "issuerUrl": "http://keycloak.test.localtest.me:9080/auth/realms/master", + "environment": "test", + "issuerUrl": "http://keycloak.localtest.me:9080/auth/realms/master", "clientRegistration": "managed", "clientId": "gwa-api", "clientSecret": "18900468-3db1-43f7-a8af-e75f079eb742" @@ -182,7 +182,7 @@ "mode": "auto", "environmentDetails": [ { - "environment": "prod", + "environment": "test", "issuerUrl": "http://keycloak.localtest.me:9080/auth/realms/master", "clientRegistration": "managed", "clientId": "gwa-api", diff --git a/e2e/cypress/fixtures/apiowner.json b/e2e/cypress/fixtures/apiowner.json index 8faee5d50..b7abb578c 100644 --- a/e2e/cypress/fixtures/apiowner.json +++ b/e2e/cypress/fixtures/apiowner.json @@ -21,7 +21,7 @@ "terms": "Terms of Use for API Gateway", "authorization": "Kong API Key with ACL Flow", "optionalInstructions": "This is a automation test", - "serviceName": "a-service-for-deleteplatform" + "serviceName": "service-for-deleteplatform" } } } diff --git a/e2e/cypress/fixtures/developer.json b/e2e/cypress/fixtures/developer.json index c15765ed2..46a88a871 100644 --- a/e2e/cypress/fixtures/developer.json +++ b/e2e/cypress/fixtures/developer.json @@ -63,6 +63,16 @@ "description": "Application for testing client credential flow." } }, + "clientIdSecret_sharedIDP": { + "product": { + "name": "Auto Test Product", + "environment": "test" + }, + "application": { + "name": "Client ID and Secret App for Shared IDP", + "description": "Application for testing client credential flow." + } + }, "clientIdSecret_invalid": { "product": { "name": "Client Credentials Test Product", diff --git a/e2e/cypress/fixtures/manage-control/kong-plugin-config.json b/e2e/cypress/fixtures/manage-control/kong-plugin-config.json index 02f34981c..9eaf278cd 100644 --- a/e2e/cypress/fixtures/manage-control/kong-plugin-config.json +++ b/e2e/cypress/fixtures/manage-control/kong-plugin-config.json @@ -17,6 +17,6 @@ "username": "consumer1" }, "keyAuth": { - "config.anonymous": "9a9e8bb2-02b1-4fe0-8b8a-591261cf1611" + "config.anonymous": "09f98d53-4797-4ba9-a9e8-6417a1cee3b0" } } \ No newline at end of file diff --git a/e2e/cypress/fixtures/state/regen.json b/e2e/cypress/fixtures/state/regen.json index 213f6856e..6636d92c9 100644 --- a/e2e/cypress/fixtures/state/regen.json +++ b/e2e/cypress/fixtures/state/regen.json @@ -1,5 +1,6 @@ { - "apikey": "NjR2PniJ6ToOVP9O7nJ5QfQiqDUhUEjq", - "consumernumber": "3CBD4EB1-9E07D3FBF2F", - "clientidsecret": "{\"clientId\": \"B297ECBB-F562BDAE027\", \"clientSecret\": \"8473aa8e-5cfd-403c-b4f6-c4ee88fc038d\", \"tokenEndpoint\": \"http://keycloak.localtest.me:9080/auth/realms/master/protocol/openid-connect/token\"}" + "apikey": "qiHHqk9lIzTgP1j94nHp5C1bNVJBOWnG", + "consumernumber": "B15172D9-F103D11A9A1", + "clientidsecret": "{\"clientId\": \"A7D72430-52CD37B2420\", \"clientSecret\": \"282718d8-d0c2-45a0-8885-3539f0ca5fea\", \"tokenEndpoint\": \"http://keycloak.localtest.me:9080/auth/realms/master/protocol/openid-connect/token\"}", + "credentials": "{\"clientId\": \"sa-ccplatform-e0000000-d339cbfcda5e\", \"clientSecret\": \"f5127730-df13-453d-ace0-a30a1255e30d\"}" } \ No newline at end of file diff --git a/e2e/cypress/fixtures/state/store.json b/e2e/cypress/fixtures/state/store.json index a9b18afe9..9e26dfeeb 100644 --- a/e2e/cypress/fixtures/state/store.json +++ b/e2e/cypress/fixtures/state/store.json @@ -1,3 +1 @@ -{ - "newapikey": "GJhaEADSMCtdUOjgErkSgtTw15KoASLu" -} \ No newline at end of file +{} \ No newline at end of file diff --git a/e2e/cypress/fixtures/test_data/authorizationProfile.json b/e2e/cypress/fixtures/test_data/authorizationProfile.json index c0f0ced55..dd3f12bd2 100644 --- a/e2e/cypress/fixtures/test_data/authorizationProfile.json +++ b/e2e/cypress/fixtures/test_data/authorizationProfile.json @@ -21,7 +21,7 @@ "clientAuthenticator": "client-secret", "name": "my-auth-client-secret", "description": "Auth connection to my IdP", - "owner": "janis@idir" + "owner": "janis@testmail.com" } }, { @@ -40,7 +40,7 @@ "clientSecret": "43badfc1-c06f-4bec-bab6-ccdc764071ac" } ], - "owner": "janis@idir" + "owner": "janis@testmail.com" } }, { @@ -59,7 +59,7 @@ "clientSecret": "43badfc1-c06f-4bec-bab6-ccdc764071ac" } ], - "owner": "janis@idir" + "owner": "janis@testmail.com" } } ] diff --git a/e2e/cypress/pageObjects/apiDirectory.ts b/e2e/cypress/pageObjects/apiDirectory.ts index c8210d1c3..350608b8a 100644 --- a/e2e/cypress/pageObjects/apiDirectory.ts +++ b/e2e/cypress/pageObjects/apiDirectory.ts @@ -1,4 +1,3 @@ -import { checkElementExists } from '../support' class ApiDirectoryPage { path: string = '/devportal/api-directory' diff --git a/e2e/cypress/pageObjects/consumers.ts b/e2e/cypress/pageObjects/consumers.ts index c8dd210d7..55de944f0 100644 --- a/e2e/cypress/pageObjects/consumers.ts +++ b/e2e/cypress/pageObjects/consumers.ts @@ -1,7 +1,7 @@ import { Assertion } from "chai" import { wrap } from "module" import dateformat from 'dateformat' -import { checkElementExists } from "../support" +import { checkElementExists } from "../support/e2e" export default class ConsumersPage { path: string = '/manager/consumers' diff --git a/e2e/cypress/pageObjects/home.ts b/e2e/cypress/pageObjects/home.ts index e417889bb..47a2df12c 100644 --- a/e2e/cypress/pageObjects/home.ts +++ b/e2e/cypress/pageObjects/home.ts @@ -16,7 +16,7 @@ class HomePage { cy.get(this.namespaceNameInput).type(name) // using `platform` as a default ns as its being seeding through feeder cy.get(this.nsCreateBtn).click() cy.verifyToastMessage("Namespace "+name+" created!") - cy.wait(2000) // wait for dropdown to have latest text + cy.wait(5000) // wait for dropdown to have latest text cy.get(this.nsDropdown).then(($el) => { expect($el).contain(name) }) @@ -26,7 +26,7 @@ class HomePage { var flag = new Boolean(false); cy.get(this.nsDropdown).click() cy.get(this.getNamespaceTestId(name)).click() - cy.wait(2000) // wait for dropdown to have latest text + cy.wait(5000) // wait for dropdown to have latest text cy.get(this.nsDropdown).then(($el) => { expect($el.text().trim()).to.eq(name) flag = true diff --git a/e2e/cypress/pageObjects/products.ts b/e2e/cypress/pageObjects/products.ts index f0b07a43c..8de0350c9 100644 --- a/e2e/cypress/pageObjects/products.ts +++ b/e2e/cypress/pageObjects/products.ts @@ -1,4 +1,4 @@ -import { checkElementExists } from "../support" + import { updateYamlDocument } from "@atomist/yaml-updater"; import _ = require("cypress/types/lodash"); const YAML = require('yamljs'); @@ -32,6 +32,7 @@ class Products { viewTemplateBtn: string = '[data-testid="edit-env-view-plugin-template-btn"]' configServiceTab: string = '[data-testid="edit-env-configure-services-tab"]' activeServicesScope: string = '[data-testid="edit-env-active-services"]' + credentialIssuer: string = '[name="credentialIssuer"]' config: string | undefined getTestIdEnvName(env: string): string { @@ -60,6 +61,21 @@ class Products { // cy.get(this.updateBtn).click() } + updateCredentialIssuer(issuerName: any) { + cy.get(this.credentialIssuer).select(`${issuerName.name} (${issuerName.environmentDetails[0].environment})`) + cy + .get(this.envCfgApprovalCheckbox) + .as('checkbox') + .invoke('is', ':checked') + .then(checked => { + cy + .get('@checkbox') + .uncheck({ force: true }); + }) + cy.get(this.envCfgApplyChangesContinueBtn).click() + cy.get(this.envCfgApplyChangesBtn).click() + } + updateOrg(orgName: string, orgUnitName: string) { cy.get(this.orgDropDown).select(orgName) cy.get(this.orgUnitDropDown).select(orgUnitName) @@ -77,9 +93,10 @@ class Products { const pname: string = productName.toLowerCase().replaceAll(' ', '-') let env = this.getTestIdEnvName(envName); cy.get(`[data-testid=${pname}-${env}-edit-btn]`).click() + cy.wait(2000) } - editProductEnvironmentConfig(config: any, invalid = false, isApproved=true) { + editProductEnvironmentConfig(config: any, invalid = false, isApproved = true) { cy.get(this.envCfgTermsDropdown).select(config.terms, { force: true }).invoke('val') @@ -93,7 +110,7 @@ class Products { authType === 'Oauth2 Client Credentials Flow' ) { let env = this.getTestIdEnvName(config.authIssuerEnv) - cy.get('[name="credentialIssuer"]').select( + cy.get(this.credentialIssuer).select( `${config.authIssuer} (${env})` ) } @@ -117,7 +134,7 @@ class Products { .check({ force: true }); } }); - cy + cy .get(this.envCfgApprovalCheckbox) .as('checkbox') .invoke('is', ':checked') @@ -191,7 +208,7 @@ class Products { cy.get(this.catelogueDropDown).type(search_input + '{downArrow}' + '{enter}', { force: true, delay: 500 - }, ) + }) // cy.get(this.catelogueDropDownMenu) // .find('div') // .find('p') diff --git a/e2e/cypress/pageObjects/serviceAccounts.ts b/e2e/cypress/pageObjects/serviceAccounts.ts index ebf387998..5999035d7 100644 --- a/e2e/cypress/pageObjects/serviceAccounts.ts +++ b/e2e/cypress/pageObjects/serviceAccounts.ts @@ -4,7 +4,7 @@ class ServiceAccountsPage { newServiceAccountBtn: string = '[data-testid=sa-create-second-btn]' clientId: string = '[data-testid=sa-new-creds-client-id]' clientSecret: string = '[data-testid=sa-new-creds-client-secret]' - serviceAccountTbl: string = '[data-testid="service-account-table"]' + serviceAccountTbl: string = '[role="table"]' serviceAcctDeleteBtn: string = '[data-testid=service-account-delete-btn]' deleteServiceAcctConfirmationBtn: string = '[data-testid="confirm-delete-service-acct-btn"]' @@ -15,34 +15,33 @@ class ServiceAccountsPage { cy.wait(8000) } -checkServiceAccountNotExist() : void - { + checkServiceAccountNotExist(): void { cy.get(this.newServiceAccountBtn).should('not.exist') } - saveServiceAcctCreds(flag?:boolean): void { + saveServiceAcctCreds(flag?: boolean): void { cy.get(this.clientId).invoke('val').then(($clientId) => { cy.get(this.clientSecret).invoke('val').then(($clientSecret) => { cy.saveState( 'credentials', '{"clientId": "' + - $clientId + - '", "clientSecret": "' + - $clientSecret + - '"}',flag + $clientId + + '", "clientSecret": "' + + $clientSecret + + '"}', flag ) }) }) } - isShareButtonVisible(expStatus : boolean) { + isShareButtonVisible(expStatus: boolean) { var actStatus = false cy.get(this.shareBtn).then($button => { if ($button.is(':visible')) - actStatus =true - assert.strictEqual (actStatus,expStatus,"Share button status is other than expected status") - }) -} + actStatus = true + assert.strictEqual(actStatus, expStatus, "Share button status is other than expected status") + }) + } selectPermissions(scopes: string[]): void { scopes.forEach((scope) => { @@ -50,10 +49,17 @@ checkServiceAccountNotExist() : void }) } - deleteAllServiceAccounts(){ + deleteAllServiceAccounts() { + cy.wait(2000) + let namespaceText cy.get(this.serviceAccountTbl).find('tr').each(($e1, index, $list) => { - cy.wrap($e1).eq(index).find(this.serviceAcctDeleteBtn).first().click() - cy.get(this.deleteServiceAcctConfirmationBtn).click() + namespaceText = $e1.find('td:nth-child(1)').text(); + cy.log('namespaceText --> '+namespaceText) + if (namespaceText.startsWith('sa')) { + cy.wrap($e1).find('button').first().click() + cy.wrap($e1).find(this.serviceAcctDeleteBtn).first().click() + cy.get(this.deleteServiceAcctConfirmationBtn).click() + } }) } } diff --git a/e2e/cypress/plugins/index.ts b/e2e/cypress/plugins/index.ts index 0eaba7a78..a8c493426 100644 --- a/e2e/cypress/plugins/index.ts +++ b/e2e/cypress/plugins/index.ts @@ -13,9 +13,13 @@ require('dotenv').config() -module.exports = (on: any, config: any) => { +module.exports = (on: any, config:any) => { + on('task', require('@cypress/code-coverage/task')) +} +// AFTER +module.exports = (on:any, config:any) => { require('@cypress/code-coverage/task')(on, config) // IMPORTANT to return the config object // with the any changed environment variables return config -} +} \ No newline at end of file diff --git a/e2e/cypress/support/auth-commands.ts b/e2e/cypress/support/auth-commands.ts index d7d2369fc..fd4a89ed2 100644 --- a/e2e/cypress/support/auth-commands.ts +++ b/e2e/cypress/support/auth-commands.ts @@ -4,9 +4,9 @@ import LoginPage from '../pageObjects/login' import request = require('request') import { method } from 'cypress/types/bluebird' import { url } from 'inspector' -import { checkElementExists } from '.' import NamespaceAccessPage from '../pageObjects/namespaceAccess' import _ = require('cypress/types/lodash') +import { checkElementExists } from './e2e' // import _ = require('cypress/types/lodash') const njwt = require('njwt') @@ -194,6 +194,7 @@ Cypress.Commands.add('logout', () => { cy.log('< Logging out') cy.getSession().then(() => { + cy.visit('/') cy.get('@session').then((res: any) => { cy.get('[data-testid=auth-menu-user]').click({ force: true }) cy.contains('Logout').click() @@ -272,6 +273,8 @@ Cypress.Commands.add('publishApi', (fileName: string, namespace: string, flag?:b Cypress.Commands.add('deleteAllCookies', () => { cy.clearCookies() + cy.clearAllLocalStorage() + cy.clearAllSessionStorage() cy.clearCookie('keystone.sid') cy.clearCookie('_oauth2_proxy') cy.exec('npm cache clear --force') diff --git a/e2e/cypress/support/index.ts b/e2e/cypress/support/e2e.ts similarity index 100% rename from e2e/cypress/support/index.ts rename to e2e/cypress/support/e2e.ts diff --git a/e2e/cypress/support/util-commands.ts b/e2e/cypress/support/util-commands.ts index 798a72b84..30aa06dd7 100644 --- a/e2e/cypress/support/util-commands.ts +++ b/e2e/cypress/support/util-commands.ts @@ -1,3 +1,4 @@ +import 'cypress-v10-preserve-cookie' const listOfCookies = [ 'AUTH_SESSION_ID_LEGACY', 'KC_RESTART', @@ -12,7 +13,7 @@ const listOfCookies = [ Cypress.Commands.add('preserveCookies', () => { cy.log('< Saving Cookies') - Cypress.Cookies.preserveOnce(...listOfCookies) + cy.preserveCookieOnce(...listOfCookies) Cypress.Cookies.debug(true) cy.log('> Saving Cookies') }) @@ -51,7 +52,7 @@ Cypress.Commands.add('saveState', (key: string, value: string, flag?: boolean, i let keyItems = key.split('>') cy.readFile('cypress/fixtures/state/store.json').then((currState) => { let newState = currState - _.set(newState, keyItems, value) + Cypress._.set(newState, keyItems, value) cy.writeFile('cypress/fixtures/state/store.json', newState) }) } @@ -88,7 +89,7 @@ Cypress.Commands.add('getState', (key: string) => { if (key.includes('>')) { let keyItems = key.split('>') cy.readFile('cypress/fixtures/state/store.json').then((state) => { - return _.get(state, keyItems) + return Cypress._.get(state, keyItems) }) } else { cy.readFile('cypress/fixtures/state/store.json').then((state) => { diff --git a/e2e/cypress/tests/01-api-key/01-create-api.spec.ts b/e2e/cypress/tests/01-api-key/01-create-api.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/01-create-api.spec.ts rename to e2e/cypress/tests/01-api-key/01-create-api.cy.ts diff --git a/e2e/cypress/tests/01-api-key/02-team-access.spec.ts b/e2e/cypress/tests/01-api-key/02-team-access.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/02-team-access.spec.ts rename to e2e/cypress/tests/01-api-key/02-team-access.cy.ts diff --git a/e2e/cypress/tests/01-api-key/03-request-access-with-out-collecting-credentials.ts b/e2e/cypress/tests/01-api-key/03-request-access-with-out-collecting-credentials.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/03-request-access-with-out-collecting-credentials.ts rename to e2e/cypress/tests/01-api-key/03-request-access-with-out-collecting-credentials.cy.ts diff --git a/e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.ts b/e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.cy.ts similarity index 92% rename from e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.ts rename to e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.cy.ts index 23c60a7da..06a133037 100644 --- a/e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.ts +++ b/e2e/cypress/tests/01-api-key/04-review-request-without-collecting-credentials.cy.ts @@ -39,4 +39,11 @@ describe('Approve Pending Request without collecting credentials Spec', () => { const flag = consumers.reviewThePendingRequest() assert.isFalse(flag, 'Review request popup is displayed') }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) + }) \ No newline at end of file diff --git a/e2e/cypress/tests/01-api-key/05-collect-credentials.spec.ts b/e2e/cypress/tests/01-api-key/05-collect-credentials.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/05-collect-credentials.spec.ts rename to e2e/cypress/tests/01-api-key/05-collect-credentials.cy.ts diff --git a/e2e/cypress/tests/01-api-key/06-approve-pending-rqst.spec.ts b/e2e/cypress/tests/01-api-key/06-approve-pending-rqst.cy.ts similarity index 97% rename from e2e/cypress/tests/01-api-key/06-approve-pending-rqst.spec.ts rename to e2e/cypress/tests/01-api-key/06-approve-pending-rqst.cy.ts index c549d34ec..e0a21c595 100644 --- a/e2e/cypress/tests/01-api-key/06-approve-pending-rqst.spec.ts +++ b/e2e/cypress/tests/01-api-key/06-approve-pending-rqst.cy.ts @@ -69,6 +69,11 @@ describe('Approve Pending Request Spec', () => { }) }) }) + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) }) // describe('Turn off the Authentication', () => { diff --git a/e2e/cypress/tests/01-api-key/07-grant-access.spec.ts b/e2e/cypress/tests/01-api-key/07-grant-access.cy.ts similarity index 94% rename from e2e/cypress/tests/01-api-key/07-grant-access.spec.ts rename to e2e/cypress/tests/01-api-key/07-grant-access.cy.ts index c49d1017e..5658cfebc 100644 --- a/e2e/cypress/tests/01-api-key/07-grant-access.spec.ts +++ b/e2e/cypress/tests/01-api-key/07-grant-access.cy.ts @@ -62,4 +62,10 @@ describe('Grant Access Spec', () => { }) }) + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) + }) \ No newline at end of file diff --git a/e2e/cypress/tests/01-api-key/08-apply-kong-api-only-consumer.ts b/e2e/cypress/tests/01-api-key/08-apply-kong-api-only-consumer.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/08-apply-kong-api-only-consumer.ts rename to e2e/cypress/tests/01-api-key/08-apply-kong-api-only-consumer.cy.ts diff --git a/e2e/cypress/tests/01-api-key/09-kong-api-only-apply-rate-limiting.ts b/e2e/cypress/tests/01-api-key/09-kong-api-only-apply-rate-limiting.cy.ts similarity index 100% rename from e2e/cypress/tests/01-api-key/09-kong-api-only-apply-rate-limiting.ts rename to e2e/cypress/tests/01-api-key/09-kong-api-only-apply-rate-limiting.cy.ts diff --git a/e2e/cypress/tests/02-manage-control/01-ip-restriction.ts b/e2e/cypress/tests/02-manage-control/01-ip-restriction.cy.ts similarity index 100% rename from e2e/cypress/tests/02-manage-control/01-ip-restriction.ts rename to e2e/cypress/tests/02-manage-control/01-ip-restriction.cy.ts diff --git a/e2e/cypress/tests/02-manage-control/02-rate-limiting.ts b/e2e/cypress/tests/02-manage-control/02-rate-limiting.cy.ts similarity index 100% rename from e2e/cypress/tests/02-manage-control/02-rate-limiting.ts rename to e2e/cypress/tests/02-manage-control/02-rate-limiting.cy.ts diff --git a/e2e/cypress/tests/03-manage-labels/01-rqst-access-for-labels.spec.ts b/e2e/cypress/tests/03-manage-labels/01-rqst-access-for-labels.cy.ts similarity index 100% rename from e2e/cypress/tests/03-manage-labels/01-rqst-access-for-labels.spec.ts rename to e2e/cypress/tests/03-manage-labels/01-rqst-access-for-labels.cy.ts diff --git a/e2e/cypress/tests/03-manage-labels/02-approve-pending-rqst-for-labels.spec copy.ts b/e2e/cypress/tests/03-manage-labels/02-approve-pending-rqst-for-labels.spec copy.cy.ts similarity index 100% rename from e2e/cypress/tests/03-manage-labels/02-approve-pending-rqst-for-labels.spec copy.ts rename to e2e/cypress/tests/03-manage-labels/02-approve-pending-rqst-for-labels.spec copy.cy.ts diff --git a/e2e/cypress/tests/03-manage-labels/03-filter-labels.spec.ts b/e2e/cypress/tests/03-manage-labels/03-filter-labels.cy.ts similarity index 100% rename from e2e/cypress/tests/03-manage-labels/03-filter-labels.spec.ts rename to e2e/cypress/tests/03-manage-labels/03-filter-labels.cy.ts diff --git a/e2e/cypress/tests/03-manage-labels/04-manage-labels-spec.ts b/e2e/cypress/tests/03-manage-labels/04-manage-labels.cy.ts similarity index 100% rename from e2e/cypress/tests/03-manage-labels/04-manage-labels-spec.ts rename to e2e/cypress/tests/03-manage-labels/04-manage-labels.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/01-client-cred-team-access.ts b/e2e/cypress/tests/04-client-credential-flow/01-client-cred-team-access.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/01-client-cred-team-access.ts rename to e2e/cypress/tests/04-client-credential-flow/01-client-cred-team-access.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.ts b/e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.cy.ts similarity index 87% rename from e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.ts rename to e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.cy.ts index 83262156b..8ad7591d6 100644 --- a/e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.ts +++ b/e2e/cypress/tests/04-client-credential-flow/02-create_authorizarion_profile.cy.ts @@ -43,6 +43,16 @@ describe('Generate Authorization Profiles', () => { cy.get(authProfile.profileTable).contains(ap.name).should('be.visible') }) }) + + it('Creates authorization profile for Client ID/Secret - Shared IDP', () => { + cy.visit(authProfile.path) + cy.get('@apiowner').then(({ clientCredentials }: any) => { + let ap = clientCredentials.clientIdSecret.authProfile + authProfile.createAuthProfile(ap) + cy.get(authProfile.profileTable).contains(ap.name).should('be.visible') + }) + }) + it('Creates authorization profile for JWT - Generated Key Pair', () => { cy.visit(authProfile.path) cy.get('@apiowner').then(({ clientCredentials }: any) => { diff --git a/e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.ts b/e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.cy.ts similarity index 98% rename from e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.ts rename to e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.cy.ts index 51d7e27eb..84170a63d 100644 --- a/e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.ts +++ b/e2e/cypress/tests/04-client-credential-flow/03-client-cred-create-api-prod-auth-pro.cy.ts @@ -107,7 +107,7 @@ describe('Create API, Product, and Authorization Profiles; Apply Auth Profiles t it('applies authorization plugin to service published to Kong Gateway', () => { cy.get('@apiowner').then(({ clientCredentials }: any) => { - cy.publishApi('cc-service.yml', clientCredentials.namespace,true).then(() => { + cy.publishApi('cc-service-plugin.yml', clientCredentials.namespace,true).then(() => { cy.get('@publishAPIResponse').then((res: any) => { cy.log(JSON.stringify(res.body)) expect(res.body.message).to.contains("Sync successful") @@ -179,6 +179,8 @@ describe('Create API, Product, and Authorization Profiles; Apply Auth Profiles t }) after(() => { + cy.visit(pd.path) + cy.logout() cy.clearLocalStorage({ log: true }) cy.deleteAllCookies() }) diff --git a/e2e/cypress/tests/04-client-credential-flow/04-cids-access-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/04-cids-access-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/04-cids-access-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/04-cids-access-rqst.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/05-cids-access-approve-api-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/05-cids-access-approve-api-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/05-cids-access-approve-api-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/05-cids-access-approve-api-rqst.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/06-jwt-genkp-access-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/06-jwt-genkp-access-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/06-jwt-genkp-access-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/06-jwt-genkp-access-rqst.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/07-jwt-genkp-access-approve-api-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/07-jwt-genkp-access-approve-api-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/07-jwt-genkp-access-approve-api-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/07-jwt-genkp-access-approve-api-rqst.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/08-jwks-url-gen-keys-access-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/08-jwks-url-gen-keys-access-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/08-jwks-url-gen-keys-access-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/08-jwks-url-gen-keys-access-rqst.cy.ts diff --git a/e2e/cypress/tests/04-client-credential-flow/09-jwks-url-access-approval-api-rqst.ts b/e2e/cypress/tests/04-client-credential-flow/09-jwks-url-access-approval-api-rqst.cy.ts similarity index 100% rename from e2e/cypress/tests/04-client-credential-flow/09-jwks-url-access-approval-api-rqst.ts rename to e2e/cypress/tests/04-client-credential-flow/09-jwks-url-access-approval-api-rqst.cy.ts diff --git a/e2e/cypress/tests/05-gateway-services/01-gateway-service-details-spec.ts b/e2e/cypress/tests/05-gateway-services/01-gateway-service-details.cy.ts similarity index 100% rename from e2e/cypress/tests/05-gateway-services/01-gateway-service-details-spec.ts rename to e2e/cypress/tests/05-gateway-services/01-gateway-service-details.cy.ts diff --git a/e2e/cypress/tests/05-gateway-services/02-filter-gateway-service.spec.ts b/e2e/cypress/tests/05-gateway-services/02-filter-gateway-service.cy.ts similarity index 100% rename from e2e/cypress/tests/05-gateway-services/02-filter-gateway-service.spec.ts rename to e2e/cypress/tests/05-gateway-services/02-filter-gateway-service.cy.ts diff --git a/e2e/cypress/tests/06-migrate-user/01-migrate-user-access.spec.ts b/e2e/cypress/tests/06-migrate-user/01-migrate-user-access.cy.ts similarity index 100% rename from e2e/cypress/tests/06-migrate-user/01-migrate-user-access.spec.ts rename to e2e/cypress/tests/06-migrate-user/01-migrate-user-access.cy.ts diff --git a/e2e/cypress/tests/07-refresh-credential/01-api-key.ts b/e2e/cypress/tests/07-refresh-credential/01-api-key.cy.ts similarity index 100% rename from e2e/cypress/tests/07-refresh-credential/01-api-key.ts rename to e2e/cypress/tests/07-refresh-credential/01-api-key.cy.ts diff --git a/e2e/cypress/tests/07-refresh-credential/02-client-credentials.ts b/e2e/cypress/tests/07-refresh-credential/02-client-credentials.cy.ts similarity index 100% rename from e2e/cypress/tests/07-refresh-credential/02-client-credentials.ts rename to e2e/cypress/tests/07-refresh-credential/02-client-credentials.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/01-client-credential-to-kong-acl-api.ts b/e2e/cypress/tests/08-update-product-env/01-client-credential-to-kong-acl-api.cy.ts similarity index 100% rename from e2e/cypress/tests/08-update-product-env/01-client-credential-to-kong-acl-api.ts rename to e2e/cypress/tests/08-update-product-env/01-client-credential-to-kong-acl-api.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/02-kong-acl-api-to-client-credential.ts b/e2e/cypress/tests/08-update-product-env/02-kong-acl-api-to-client-credential.cy.ts similarity index 100% rename from e2e/cypress/tests/08-update-product-env/02-kong-acl-api-to-client-credential.ts rename to e2e/cypress/tests/08-update-product-env/02-kong-acl-api-to-client-credential.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/03-apply-multiple-services.ts b/e2e/cypress/tests/08-update-product-env/03-apply-multiple-services.cy.ts similarity index 100% rename from e2e/cypress/tests/08-update-product-env/03-apply-multiple-services.ts rename to e2e/cypress/tests/08-update-product-env/03-apply-multiple-services.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/04-change-env-status copy.ts b/e2e/cypress/tests/08-update-product-env/04-change-env-status copy.cy.ts similarity index 100% rename from e2e/cypress/tests/08-update-product-env/04-change-env-status copy.ts rename to e2e/cypress/tests/08-update-product-env/04-change-env-status copy.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/05-keycloak-shared-IDP-config.ts b/e2e/cypress/tests/08-update-product-env/05-keycloak-shared-IDP-config.cy.ts similarity index 100% rename from e2e/cypress/tests/08-update-product-env/05-keycloak-shared-IDP-config.ts rename to e2e/cypress/tests/08-update-product-env/05-keycloak-shared-IDP-config.cy.ts diff --git a/e2e/cypress/tests/08-update-product-env/06-shared-idp.cy.ts b/e2e/cypress/tests/08-update-product-env/06-shared-idp.cy.ts new file mode 100644 index 000000000..0839455dd --- /dev/null +++ b/e2e/cypress/tests/08-update-product-env/06-shared-idp.cy.ts @@ -0,0 +1,290 @@ +import ApiDirectoryPage from '../../pageObjects/apiDirectory' +import ApplicationPage from '../../pageObjects/applications' +import AuthorizationProfile from '../../pageObjects/authProfile' +import ConsumersPage from '../../pageObjects/consumers' +import HomePage from '../../pageObjects/home' +import keycloakGroupPage from '../../pageObjects/keycloakGroup' +import KeycloakUserGroupPage from '../../pageObjects/keycloakUserGroup' +import LoginPage from '../../pageObjects/login' +import MyAccessPage from '../../pageObjects/myAccess' +import Products from '../../pageObjects/products' + +describe('Apply Shared IDP while creating Authorization Profile', () => { + const login = new LoginPage() + var nameSpace: string + const home = new HomePage() + const authProfile = new AuthorizationProfile() + let userSession: string + + before(() => { + cy.visit('/') + cy.deleteAllCookies() + cy.reload() + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('developer').as('developer') + cy.fixture('apiowner').as('apiowner') + cy.fixture('api').as('api') + cy.fixture('state/regen').as('regen') + cy.visit(login.path) + }) + + it('authenticates Janis (api owner) to get the user session token', () => { + cy.getUserSession().then(() => { + cy.get('@apiowner').then(({ user, namespace }: any) => { + cy.login(user.credentials.username, user.credentials.password) + home.useNamespace(namespace) + cy.get('@login').then(function (xhr: any) { + userSession = xhr.response.headers['x-auth-request-access-token'] + }) + }) + }) + }) + + it('Prepare the Request Specification for the API', () => { + cy.get('@api').then(({ authorizationProfiles }: any) => { + cy.setHeaders(authorizationProfiles.headers) + cy.setAuthorizationToken(userSession) + cy.setRequestBody(authorizationProfiles.shared_IDP_body) + }) + }) + + it('Publish the Shared IDP profile', () => { + cy.get('@apiowner').then(({ namespace }: any) => { + cy.makeAPIRequest('ds/api/v2/namespaces/' + namespace + '/issuers', 'PUT').then((response) => { + expect(response.status).to.be.equal(200) + expect(response.body.result).to.be.contain('created') + }) + }) + }) + + it('Create an authorization profile and associate it with shared IPD', () => { + cy.visit(authProfile.path) + cy.get('@apiowner').then(({ clientCredentials }: any) => { + let ap = clientCredentials.sharedIDP.authProfile + authProfile.createAuthProfile(ap) + cy.get(authProfile.profileTable).contains(ap.name).should('be.visible') + }) + }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) + +}) + +describe('Update IDP issuer for shared IDP profile', () => { + + const login = new LoginPage() + const home = new HomePage() + let userSession: string + const authProfile = new AuthorizationProfile() + + before(() => { + cy.visit('/') + cy.deleteAllCookies() + cy.reload() + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('apiowner').as('apiowner') + cy.fixture('api').as('api') + cy.visit(login.path) + }) + + it('authenticates Janis (api owner) to get the user session token', () => { + cy.getUserSession().then(() => { + cy.get('@apiowner').then(({ user, namespace }: any) => { + cy.login(user.credentials.username, user.credentials.password) + home.useNamespace(namespace) + cy.get('@login').then(function (xhr: any) { + userSession = xhr.response.headers['x-auth-request-access-token'] + }) + }) + }) + }) + + it('Prepare the Request Specification for the API', () => { + cy.get('@api').then(({ authorizationProfiles }: any) => { + cy.setHeaders(authorizationProfiles.headers) + cy.setAuthorizationToken(userSession) + cy.setRequestBody(authorizationProfiles.shared_IDP_update_body) + }) + }) + + it('Put the resource and verify the success code in the response', () => { + cy.get('@apiowner').then(({ namespace }: any) => { + cy.makeAPIRequest('ds/api/v2/namespaces/' + namespace + '/issuers', 'PUT').then((response) => { + expect(response.status).to.be.equal(200) + }) + }) + }) + + it('Edit the created profile and verify the updated Issuer URL', () => { + cy.visit(authProfile.path) + cy.get('@apiowner').then(({ clientCredentials }: any) => { + let ap = clientCredentials.sharedIDP.authProfile + authProfile.editAuthorizationProfile(ap.name) + cy.wait(2000) + cy.get('@api').then(({ authorizationProfiles }: any) => { + authProfile.verifyAuthorizationProfileIssuerURL(authorizationProfiles.shared_IDP_update_body.environmentDetails[0].issuerUrl) + }) + }) + }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) + +}) + +describe('Update IDP issuer for shared IDP profile', () => { + + const login = new LoginPage() + const home = new HomePage() + let userSession: string + const authProfile = new AuthorizationProfile() + const pd = new Products() + + before(() => { + cy.visit('/') + cy.deleteAllCookies() + cy.reload() + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('apiowner').as('apiowner') + cy.fixture('api').as('api') + cy.visit(login.path) + }) + + it('authenticates Janis (api owner) to get the user session token', () => { + cy.getUserSession().then(() => { + cy.get('@apiowner').then(({ user, namespace }: any) => { + cy.login(user.credentials.username, user.credentials.password) + home.useNamespace(namespace) + }) + }) + }) + + it('Update the Shared IDP Profile to an active clientID Secret auth environment', () => { + cy.visit(pd.path) + cy.get('@apiowner').then(({ product }: any) => { + pd.editProductEnvironment(product.name, product.test_environment.name) + cy.get('@api').then(({ authorizationProfiles }: any) => { + pd.updateCredentialIssuer(authorizationProfiles.shared_IDP_update_body) + }) + }) + }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) +}) + +describe('Developer creates an access request for Client ID/Secret authenticator', () => { + const login = new LoginPage() + const apiDir = new ApiDirectoryPage() + const app = new ApplicationPage() + const ma = new MyAccessPage() + + before(() => { + cy.visit('/') + cy.deleteAllCookies() + cy.reload() + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('developer').as('developer') + // cy.visit(login.path) + }) + + it('Developer logs in', () => { + cy.get('@developer').then(({ user }: any) => { + cy.login(user.credentials.username, user.credentials.password) + }) + }) + + it('Creates an application', () => { + cy.visit(app.path) + cy.get('@developer').then(({ clientCredentials }: any) => { + app.createApplication(clientCredentials.clientIdSecret_sharedIDP.application) + }) + }) + + it('Creates an access request', () => { + cy.visit(apiDir.path) + cy.get('@developer').then(({ clientCredentials, accessRequest }: any) => { + let product = clientCredentials.clientIdSecret_sharedIDP.product + let app = clientCredentials.clientIdSecret_sharedIDP.application + + apiDir.createAccessRequest(product, app, accessRequest) + ma.clickOnGenerateSecretButton() + + cy.contains('Client ID').should('be.visible') + cy.contains('Client Secret').should('be.visible') + cy.contains('Token Endpoint').should('be.visible') + cy.log(Cypress.env('clientidsecret')) + ma.saveClientCredentials(false, true) + }) + }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) +}) + +describe('Verify that the service is accessible using new Client ID, Secret, and Access Token', () => { + let token: string + it('Get access token using client ID and secret; make API request for test environment', () => { + cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { + + let cc = JSON.parse(store_res.clientidsecret) + // let cc = JSON.parse(Cypress.env('clientidsecret')) + cy.log('cc-->' + cc.clientSecret) + cy.getAccessToken(cc.clientId, cc.clientSecret).then(() => { + cy.get('@accessTokenResponse').then((token_res: any) => { + token = token_res.body.access_token + cy.request({ + url: Cypress.env('KONG_URL'), + headers: { + Host: 'a-service-for-newplatform-test.api.gov.bc.ca', + }, + auth: { + bearer: token, + }, + }).then((res) => { + expect(res.status).to.eq(200) + }) + }) + }) + }) + }) + + it('Get access token using client ID and secret; make API request for Dev', () => { + cy.request({ + url: Cypress.env('KONG_URL'), + headers: { + Host: 'a-service-for-newplatform.api.gov.bc.ca', + }, + auth: { + bearer: token, + }, + }).then((res) => { + expect(res.status).to.eq(200) + }) + }) +}) diff --git a/e2e/cypress/tests/08-update-product-env/06-shared-idp.ts b/e2e/cypress/tests/08-update-product-env/06-shared-idp.ts deleted file mode 100644 index c56d84a7c..000000000 --- a/e2e/cypress/tests/08-update-product-env/06-shared-idp.ts +++ /dev/null @@ -1,139 +0,0 @@ -import ApiDirectoryPage from '../../pageObjects/apiDirectory' -import ApplicationPage from '../../pageObjects/applications' -import AuthorizationProfile from '../../pageObjects/authProfile' -import ConsumersPage from '../../pageObjects/consumers' -import HomePage from '../../pageObjects/home' -import keycloakGroupPage from '../../pageObjects/keycloakGroup' -import KeycloakUserGroupPage from '../../pageObjects/keycloakUserGroup' -import LoginPage from '../../pageObjects/login' -import MyAccessPage from '../../pageObjects/myAccess' -import Products from '../../pageObjects/products' - -describe('Apply Shared IDP while creating Authorization Profile', () => { - const login = new LoginPage() - var nameSpace: string - const home = new HomePage() - const authProfile = new AuthorizationProfile() - let userSession: string - - before(() => { - cy.visit('/') - cy.deleteAllCookies() - cy.reload() - }) - - beforeEach(() => { - cy.preserveCookies() - cy.fixture('developer').as('developer') - cy.fixture('apiowner').as('apiowner') - cy.fixture('api').as('api') - cy.fixture('state/regen').as('regen') - cy.visit(login.path) - }) - - it('authenticates Janis (api owner) to get the user session token', () => { - cy.getUserSession().then(() => { - cy.get('@apiowner').then(({ user, namespace }: any) => { - cy.login(user.credentials.username, user.credentials.password) - home.useNamespace(namespace) - cy.get('@login').then(function (xhr: any) { - userSession = xhr.response.headers['x-auth-request-access-token'] - }) - }) - }) - }) - - it('Prepare the Request Specification for the API', () => { - cy.get('@api').then(({ authorizationProfiles }: any) => { - cy.setHeaders(authorizationProfiles.headers) - cy.setAuthorizationToken(userSession) - cy.setRequestBody(authorizationProfiles.shared_IDP_body) - }) - }) - - it('Publish the Shared IDP profile', () => { - cy.get('@apiowner').then(({ namespace }: any) => { - cy.makeAPIRequest('ds/api/v2/namespaces/'+namespace+'/issuers', 'PUT').then((response) => { - expect(response.status).to.be.equal(200) - expect(response.body.result).to.be.contain('created') - }) - }) - }) - - it('Create an authorization profile and associate it with shared IPD', () => { - cy.visit(authProfile.path) - cy.get('@apiowner').then(({ clientCredentials }: any) => { - let ap = clientCredentials.sharedIDP.authProfile - authProfile.createAuthProfile(ap) - cy.get(authProfile.profileTable).contains(ap.name).should('be.visible') - }) - }) - - after(() => { - cy.logout() - cy.clearLocalStorage({ log: true }) - cy.deleteAllCookies() - }) - -}) - -describe('Update IDP issuer for shared IDP profile', () => { - - const login = new LoginPage() - const home = new HomePage() - let userSession: string - const authProfile = new AuthorizationProfile() - - before(() => { - cy.visit('/') - cy.deleteAllCookies() - cy.reload() - }) - - beforeEach(() => { - cy.preserveCookies() - cy.fixture('apiowner').as('apiowner') - cy.fixture('api').as('api') - cy.visit(login.path) - }) - - it('authenticates Janis (api owner) to get the user session token', () => { - cy.getUserSession().then(() => { - cy.get('@apiowner').then(({ user, namespace }: any) => { - cy.login(user.credentials.username, user.credentials.password) - home.useNamespace(namespace) - cy.get('@login').then(function (xhr: any) { - userSession = xhr.response.headers['x-auth-request-access-token'] - }) - }) - }) - }) - - it('Prepare the Request Specification for the API', () => { - cy.get('@api').then(({ authorizationProfiles }: any) => { - cy.setHeaders(authorizationProfiles.headers) - cy.setAuthorizationToken(userSession) - cy.setRequestBody(authorizationProfiles.shared_IDP_update_body) - }) - }) - - it('Put the resource and verify the success code in the response', () => { - cy.get('@apiowner').then(({ namespace }: any) => { - cy.makeAPIRequest('ds/api/v2/namespaces/'+namespace+'/issuers', 'PUT').then((response) => { - expect(response.status).to.be.equal(200) - }) - }) - }) - - it('Edit the created profile and verify the updated Issuer URL', () => { - cy.visit(authProfile.path) - cy.get('@apiowner').then(({ clientCredentials }: any) => { - let ap = clientCredentials.sharedIDP.authProfile - authProfile.editAuthorizationProfile(ap.name) - cy.wait(2000) - cy.get('@api').then(({ authorizationProfiles }: any) => { - authProfile.verifyAuthorizationProfileIssuerURL(authorizationProfiles.shared_IDP_update_body.environmentDetails[0].issuerUrl) - }) - }) - }) -}) \ No newline at end of file diff --git a/e2e/cypress/tests/09-clear-resources/01-create-api.spec.ts b/e2e/cypress/tests/09-clear-resources/01-create-api.cy.ts similarity index 67% rename from e2e/cypress/tests/09-clear-resources/01-create-api.spec.ts rename to e2e/cypress/tests/09-clear-resources/01-create-api.cy.ts index 10d7944a6..d531bc523 100644 --- a/e2e/cypress/tests/09-clear-resources/01-create-api.spec.ts +++ b/e2e/cypress/tests/09-clear-resources/01-create-api.cy.ts @@ -3,7 +3,7 @@ import LoginPage from '../../pageObjects/login' import Products from '../../pageObjects/products' import ServiceAccountsPage from '../../pageObjects/serviceAccounts' -describe('Create API Spec', () => { +describe('Create API Spec for Delete Resources', () => { const login = new LoginPage() const home = new HomePage() const sa = new ServiceAccountsPage() @@ -22,7 +22,7 @@ describe('Create API Spec', () => { cy.preserveCookies() cy.fixture('apiowner').as('apiowner') cy.fixture('api').as('api') - cy.visit(login.path) + // cy.visit(login.path) }) it('authenticates Janis (api owner)', () => { @@ -53,7 +53,7 @@ describe('Create API Spec', () => { it('publishes a new API to Kong Gateway', () => { cy.get('@apiowner').then(({ deleteResources }: any) => { - cy.publishApi('service.yml', deleteResources.namespace).then(() => { + cy.publishApi('service-clear-resources.yml', deleteResources.namespace).then(() => { cy.get('@publishAPIResponse').then((res: any) => { cy.log(JSON.stringify(res.body)) }) @@ -85,24 +85,33 @@ describe('Create API Spec', () => { }) }) - // it('publish product to directory', () => { - // cy.visit(pd.path) - // cy.get('@apiowner').then(({ deleteResources }: any) => { - // pd.editProductEnvironment(deleteResources.product.name, deleteResources.product.environment.name) - // pd.editProductEnvironmentConfig(deleteResources.product.environment.config) - // pd.generateKongPluginConfig(deleteResources.product.name, deleteResources.product.environment.name,'service-clear-resources.yml') - // }) - // }) + it('publish product to directory', () => { + cy.visit(sa.path) + cy.visit(pd.path) + cy.get('@apiowner').then(({ deleteResources }: any) => { + pd.editProductEnvironment(deleteResources.product.name, deleteResources.product.environment.name) + pd.editProductEnvironmentConfig(deleteResources.product.environment.config) + pd.generateKongPluginConfig(deleteResources.product.name, deleteResources.product.environment.name,'service-clear-resources.yml') + }) + }) - // it('applies authorization plugin to service published to Kong Gateway', () => { - // cy.get('@apiowner').then(({ deleteResources }: any) => { - // cy.publishApi('service-clear-resources-plugin.yml', deleteResources.namespace).then(() => { - // cy.get('@publishAPIResponse').then((res: any) => { - // cy.log(JSON.stringify(res.body)) - // }) - // }) - // }) - // }) + it('applies authorization plugin to service published to Kong Gateway', () => { + cy.get('@apiowner').then(({ deleteResources }: any) => { + cy.publishApi('service-clear-resources-plugin.yml', deleteResources.namespace).then(() => { + cy.get('@publishAPIResponse').then((res: any) => { + cy.log(JSON.stringify(res.body)) + }) + }) + }) + }) + + it('activate the service for Dev environment', () => { + cy.visit(pd.path) + cy.get('@apiowner').then(({ deleteResources }: any) => { + pd.activateService(deleteResources.product.name, deleteResources.product.environment.name,deleteResources.product.environment.config) + cy.wait(3000) + }) + }) after(() => { cy.logout() diff --git a/e2e/cypress/tests/09-clear-resources/02-delete-resources.ts b/e2e/cypress/tests/09-clear-resources/02-delete-resources.cy.ts similarity index 70% rename from e2e/cypress/tests/09-clear-resources/02-delete-resources.ts rename to e2e/cypress/tests/09-clear-resources/02-delete-resources.cy.ts index bdcd47c1c..a8ca538f4 100644 --- a/e2e/cypress/tests/09-clear-resources/02-delete-resources.ts +++ b/e2e/cypress/tests/09-clear-resources/02-delete-resources.cy.ts @@ -10,6 +10,7 @@ describe('Delete created resources', () => { const sa = new ServiceAccountsPage() const pd = new Products() const ns = new NameSpacePage + let flag: boolean before(() => { cy.visit('/') @@ -49,22 +50,35 @@ describe('Delete created resources', () => { }) }) - // it('Delete Service Accounts', () => { - // cy.visit(sa.path) - // sa.deleteAllServiceAccounts() - // }) + it('Delete Service Accounts', () => { + cy.visit(sa.path) + sa.deleteAllServiceAccounts() + }) it('Delete Namespace', () => { cy.get('@apiowner').then(({ deleteResources }: any) => { cy.visit(ns.path) - // ns.deleteNamespace(deleteResources.namespace) + ns.deleteNamespace(deleteResources.namespace) + }) + }) + + it('Verify that the deleted namespace does not display in namespace list', () => { + cy.on('fail', (err, runnable) => { + flag = false + }) + cy.get('@apiowner').then(({ deleteResources }: any) => { + flag = true + home.useNamespace(deleteResources.namespace) }) }) - // it('Verify that the deleted namespace does not display in namespace list', () => { - // cy.get('@apiowner').then(({ deleteResources }: any) => { - // const flag = home.useNamespace(deleteResources.namespace) - // assert.equal(flag, false) - // }) - // }) + it('Verify that the namespace is deleted', () => { + assert.equal(flag, false) + }) + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) }) diff --git a/e2e/cypress/tests/10-activity-feed/01-activity-feed.ts b/e2e/cypress/tests/10-activity-feed/01-activity-feed.cy.ts similarity index 100% rename from e2e/cypress/tests/10-activity-feed/01-activity-feed.ts rename to e2e/cypress/tests/10-activity-feed/01-activity-feed.cy.ts diff --git a/e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.ts b/e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.cy.ts similarity index 97% rename from e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.ts rename to e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.cy.ts index 1e88454cc..a7c69dc4e 100644 --- a/e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.ts +++ b/e2e/cypress/tests/10-activity-feed/02-activity-feed-failure.cy.ts @@ -54,6 +54,11 @@ describe('Make the access request for invalid profile', () => { // // ma.closeRequestAccessPopUp() // }) // }) + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) describe('Create API, Product, and Authorization Profiles; Apply Auth Profiles to Product Environments', () => { diff --git a/e2e/cypress/tests/11-access-permission/01-create-api.spec.ts b/e2e/cypress/tests/11-access-permission/01-create-api.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/01-create-api.spec.ts rename to e2e/cypress/tests/11-access-permission/01-create-api.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/02-team-access.spec.ts b/e2e/cypress/tests/11-access-permission/02-team-access.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/02-team-access.spec.ts rename to e2e/cypress/tests/11-access-permission/02-team-access.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/03-rqst-access.spec.ts b/e2e/cypress/tests/11-access-permission/03-rqst-access.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/03-rqst-access.spec.ts rename to e2e/cypress/tests/11-access-permission/03-rqst-access.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/04-access-manager.ts b/e2e/cypress/tests/11-access-permission/04-access-manager.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/04-access-manager.ts rename to e2e/cypress/tests/11-access-permission/04-access-manager.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/05-namespace-manage.ts b/e2e/cypress/tests/11-access-permission/05-namespace-manage.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/05-namespace-manage.ts rename to e2e/cypress/tests/11-access-permission/05-namespace-manage.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/06-credential-issuer.ts b/e2e/cypress/tests/11-access-permission/06-credential-issuer.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/06-credential-issuer.ts rename to e2e/cypress/tests/11-access-permission/06-credential-issuer.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/07-namespace-view.ts b/e2e/cypress/tests/11-access-permission/07-namespace-view.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/07-namespace-view.ts rename to e2e/cypress/tests/11-access-permission/07-namespace-view.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/08-gateway-config.ts b/e2e/cypress/tests/11-access-permission/08-gateway-config.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/08-gateway-config.ts rename to e2e/cypress/tests/11-access-permission/08-gateway-config.cy.ts diff --git a/e2e/cypress/tests/11-access-permission/09-content-publish.ts b/e2e/cypress/tests/11-access-permission/09-content-publish.cy.ts similarity index 100% rename from e2e/cypress/tests/11-access-permission/09-content-publish.ts rename to e2e/cypress/tests/11-access-permission/09-content-publish.cy.ts diff --git a/e2e/cypress/tests/12-delete-application/01-delete-application-without-access.ts b/e2e/cypress/tests/12-delete-application/01-delete-application-without-access.cy.ts similarity index 100% rename from e2e/cypress/tests/12-delete-application/01-delete-application-without-access.ts rename to e2e/cypress/tests/12-delete-application/01-delete-application-without-access.cy.ts diff --git a/e2e/cypress/tests/12-delete-application/02-delete-application-with-pending-request.ts b/e2e/cypress/tests/12-delete-application/02-delete-application-with-pending-request.cy.ts similarity index 100% rename from e2e/cypress/tests/12-delete-application/02-delete-application-with-pending-request.ts rename to e2e/cypress/tests/12-delete-application/02-delete-application-with-pending-request.cy.ts diff --git a/e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.ts b/e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.cy.ts similarity index 99% rename from e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.ts rename to e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.cy.ts index a058588cb..b1c91207c 100644 --- a/e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.ts +++ b/e2e/cypress/tests/12-delete-application/03-delete-application-with-approved-request.cy.ts @@ -47,6 +47,7 @@ describe('Delete application which has approved request spec', () => { }) after(() => { + cy.logout() cy.clearLocalStorage({ log: true }) cy.deleteAllCookies() }) diff --git a/e2e/cypress/tests/13-namespace-preview-mode/01-create-api.spec.ts b/e2e/cypress/tests/13-namespace-preview-mode/01-create-api.cy.ts similarity index 100% rename from e2e/cypress/tests/13-namespace-preview-mode/01-create-api.spec.ts rename to e2e/cypress/tests/13-namespace-preview-mode/01-create-api.cy.ts diff --git a/e2e/cypress/tests/13-namespace-preview-mode/02-namespace-preview-mode.ts b/e2e/cypress/tests/13-namespace-preview-mode/02-namespace-preview-mode.cy.ts similarity index 100% rename from e2e/cypress/tests/13-namespace-preview-mode/02-namespace-preview-mode.ts rename to e2e/cypress/tests/13-namespace-preview-mode/02-namespace-preview-mode.cy.ts diff --git a/e2e/cypress/tests/14-aps-api/01-create-api.spec.ts b/e2e/cypress/tests/14-aps-api/01-create-api.cy.ts similarity index 100% rename from e2e/cypress/tests/14-aps-api/01-create-api.spec.ts rename to e2e/cypress/tests/14-aps-api/01-create-api.cy.ts diff --git a/e2e/cypress/tests/14-aps-api/02-organization.ts b/e2e/cypress/tests/14-aps-api/02-organization.cy.ts similarity index 98% rename from e2e/cypress/tests/14-aps-api/02-organization.ts rename to e2e/cypress/tests/14-aps-api/02-organization.cy.ts index 0a0a4afd7..4d549b454 100644 --- a/e2e/cypress/tests/14-aps-api/02-organization.ts +++ b/e2e/cypress/tests/14-aps-api/02-organization.cy.ts @@ -244,4 +244,10 @@ describe('Add and Get Organization Access', () => { cy.compareJSONObjects(response, organization.body) }) }) + + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) \ No newline at end of file diff --git a/e2e/cypress/tests/14-aps-api/03-documentation.ts b/e2e/cypress/tests/14-aps-api/03-documentation.cy.ts similarity index 98% rename from e2e/cypress/tests/14-aps-api/03-documentation.ts rename to e2e/cypress/tests/14-aps-api/03-documentation.cy.ts index 28bee039f..b1d02226e 100644 --- a/e2e/cypress/tests/14-aps-api/03-documentation.ts +++ b/e2e/cypress/tests/14-aps-api/03-documentation.cy.ts @@ -224,4 +224,10 @@ describe('API Tests to verify Get documentation content', () => { }) }) }) + + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) diff --git a/e2e/cypress/tests/14-aps-api/04-authorizationProfiles.ts b/e2e/cypress/tests/14-aps-api/04-authorizationProfiles.cy.ts similarity index 97% rename from e2e/cypress/tests/14-aps-api/04-authorizationProfiles.ts rename to e2e/cypress/tests/14-aps-api/04-authorizationProfiles.cy.ts index f2b8c224d..d07a312c0 100644 --- a/e2e/cypress/tests/14-aps-api/04-authorizationProfiles.ts +++ b/e2e/cypress/tests/14-aps-api/04-authorizationProfiles.cy.ts @@ -91,4 +91,9 @@ testData.forEach((testCase: any) => { }) }) }) + + after(() => { + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) \ No newline at end of file diff --git a/e2e/cypress/tests/14-aps-api/05-products.ts b/e2e/cypress/tests/14-aps-api/05-products.cy.ts similarity index 97% rename from e2e/cypress/tests/14-aps-api/05-products.ts rename to e2e/cypress/tests/14-aps-api/05-products.cy.ts index bc9d563f2..89fc847ca 100644 --- a/e2e/cypress/tests/14-aps-api/05-products.ts +++ b/e2e/cypress/tests/14-aps-api/05-products.cy.ts @@ -167,4 +167,10 @@ describe('API Tests for Delete Products', () => { }) }) }) + + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) \ No newline at end of file diff --git a/e2e/cypress/tests/14-aps-api/06-api-directory.ts b/e2e/cypress/tests/14-aps-api/06-api-directory.cy.ts similarity index 98% rename from e2e/cypress/tests/14-aps-api/06-api-directory.ts rename to e2e/cypress/tests/14-aps-api/06-api-directory.cy.ts index 34cac8593..8a17c8033 100644 --- a/e2e/cypress/tests/14-aps-api/06-api-directory.ts +++ b/e2e/cypress/tests/14-aps-api/06-api-directory.cy.ts @@ -232,4 +232,10 @@ describe('API Tests for Updating dataset', () => { }) }) }) + + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) \ No newline at end of file diff --git a/e2e/cypress/tests/14-aps-api/07-namespaces.ts b/e2e/cypress/tests/14-aps-api/07-namespaces.cy.ts similarity index 98% rename from e2e/cypress/tests/14-aps-api/07-namespaces.ts rename to e2e/cypress/tests/14-aps-api/07-namespaces.cy.ts index 94a575928..8b542e37a 100644 --- a/e2e/cypress/tests/14-aps-api/07-namespaces.ts +++ b/e2e/cypress/tests/14-aps-api/07-namespaces.cy.ts @@ -198,4 +198,10 @@ describe('API Tests for Deleting Namespace', () => { // }) // }) // }) + + after(() => { + cy.logout() + cy.clearLocalStorage({log:true}) + cy.deleteAllCookies() + }) }) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 29b3aab91..540504482 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -37,8 +37,9 @@ "@types/request": "^2.48.7", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", - "cypress": "^9.7.0", - "cypress-keycloak": "^1.7.0", + "cypress": "^12.4.0", + "cypress-mochawesome-reporter": "^3.2.3", + "cypress-v10-preserve-cookie": "^1.2.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-chai-friendly": "^0.7.1", "eslint-plugin-cypress": "^2.11.3", @@ -3438,9 +3439,9 @@ } }, "node_modules/cypress": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.7.0.tgz", - "integrity": "sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.4.0.tgz", + "integrity": "sha512-//h93K/yGC/7pxv1KamlkADbKHLp5h3f9rZDE2McRjXZDagMETH0sXowOOanvhsH8cFt/JWspIcK+p9cuaoAqg==", "hasInstallScript": true, "dependencies": { "@cypress/request": "^2.88.10", @@ -3462,7 +3463,7 @@ "dayjs": "^1.10.4", "debug": "^4.3.2", "enquirer": "^2.3.6", - "eventemitter2": "^6.4.3", + "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", @@ -3490,18 +3491,73 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=12.0.0" + "node": "^14.0.0 || ^16.0.0 || >=18.0.0" } }, - "node_modules/cypress-keycloak": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/cypress-keycloak/-/cypress-keycloak-1.8.0.tgz", - "integrity": "sha512-ZjdVI6cUc7Jnvf2jw8/cFc2XHOHbWt6q9RiudYOHF1ykuKO33Evb6iXHXF4JnIYAClp2EZw/Ewy3aizrTUO6XQ==", + "node_modules/cypress-mochawesome-reporter": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/cypress-mochawesome-reporter/-/cypress-mochawesome-reporter-3.2.3.tgz", + "integrity": "sha512-gUtIYBH+KvnAeCkTFu/t9niX1KHnAbEZyxBFvEHQRtrQNXLERjLJjGOHIW6kuESIdVMiEFOcvuNF2LuBpORW9A==", "dev": true, + "dependencies": { + "fs-extra": "^10.0.1", + "mochawesome": "^7.1.3", + "mochawesome-merge": "^4.2.1", + "mochawesome-report-generator": "^6.2.0" + }, + "engines": { + "node": ">=14" + }, "peerDependencies": { - "cypress": "^6.5.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + "cypress": ">=6.2.0" } }, + "node_modules/cypress-mochawesome-reporter/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cypress-v10-preserve-cookie": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cypress-v10-preserve-cookie/-/cypress-v10-preserve-cookie-1.2.1.tgz", + "integrity": "sha512-8JAfcrDrCCiFyce7ygtqO9Gad0yzFla9WNDGA9j4sz6kLSvh2m2fWcBcON2VwWw5GfRDXmpe+yzT2r8Mu3jGdA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + } + }, + "node_modules/cypress-v10-preserve-cookie/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/cypress-v10-preserve-cookie/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/cypress-xpath": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/cypress-xpath/-/cypress-xpath-1.6.2.tgz", @@ -4455,9 +4511,9 @@ } }, "node_modules/eventemitter2": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz", - "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==" + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==" }, "node_modules/events": { "version": "2.1.0", @@ -11960,9 +12016,9 @@ } }, "cypress": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.7.0.tgz", - "integrity": "sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.4.0.tgz", + "integrity": "sha512-//h93K/yGC/7pxv1KamlkADbKHLp5h3f9rZDE2McRjXZDagMETH0sXowOOanvhsH8cFt/JWspIcK+p9cuaoAqg==", "requires": { "@cypress/request": "^2.88.10", "@cypress/xvfb": "^1.2.4", @@ -11983,7 +12039,7 @@ "dayjs": "^1.10.4", "debug": "^4.3.2", "enquirer": "^2.3.6", - "eventemitter2": "^6.4.3", + "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", @@ -12040,12 +12096,56 @@ } } }, - "cypress-keycloak": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/cypress-keycloak/-/cypress-keycloak-1.8.0.tgz", - "integrity": "sha512-ZjdVI6cUc7Jnvf2jw8/cFc2XHOHbWt6q9RiudYOHF1ykuKO33Evb6iXHXF4JnIYAClp2EZw/Ewy3aizrTUO6XQ==", + "cypress-mochawesome-reporter": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/cypress-mochawesome-reporter/-/cypress-mochawesome-reporter-3.2.3.tgz", + "integrity": "sha512-gUtIYBH+KvnAeCkTFu/t9niX1KHnAbEZyxBFvEHQRtrQNXLERjLJjGOHIW6kuESIdVMiEFOcvuNF2LuBpORW9A==", "dev": true, - "requires": {} + "requires": { + "fs-extra": "^10.0.1", + "mochawesome": "^7.1.3", + "mochawesome-merge": "^4.2.1", + "mochawesome-report-generator": "^6.2.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "cypress-v10-preserve-cookie": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cypress-v10-preserve-cookie/-/cypress-v10-preserve-cookie-1.2.1.tgz", + "integrity": "sha512-8JAfcrDrCCiFyce7ygtqO9Gad0yzFla9WNDGA9j4sz6kLSvh2m2fWcBcON2VwWw5GfRDXmpe+yzT2r8Mu3jGdA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } }, "cypress-xpath": { "version": "1.6.2", @@ -12748,9 +12848,9 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "eventemitter2": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz", - "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==" + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==" }, "events": { "version": "2.1.0", diff --git a/e2e/package.json b/e2e/package.json index ab6638afa..a62f15243 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -6,12 +6,12 @@ "author": "BC Gov APS", "license": "MIT", "scripts": { - "cy:open": "cypress open --config-file cypress.json", - "cy:run": "cypress run --config-file cypress.json --env TransferProtocol='http' --browser chrome", + "cy:open": "cypress open --config-file=cypress.config.ts", + "cy:run": "npx cypress run --headed --config-file cypress.config.ts --env TransferProtocol='http' --browser chrome", "cy:run:rcd": "npm run cy:run -- --record", "moch:json": "mochawesome-merge -f results/*.json -o results/bcgov-aps-e2e-report.json", "cy:chromium": "cypress open --browser /usr/bin/chromium", - "cy:edge": "npx cypress run --browser edge", + "cy:edge": "npx cypress run --headed --browser edge", "moch:html": "marge results/bcgov-aps-e2e-report.json --reportDir results/report -i -t \"API Services Portal E2E Test Report\"", "cy:chrome": "cypress run --headed --env TransferProtocol='http' --browser chrome", "cy:run:html": "run-s --continue-on-error cy:run moch:json moch:html", @@ -29,8 +29,9 @@ "@types/request": "^2.48.7", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", - "cypress": "^9.7.0", - "cypress-keycloak": "^1.7.0", + "cypress": "^12.4.0", + "cypress-mochawesome-reporter": "^3.2.3", + "cypress-v10-preserve-cookie": "^1.2.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-chai-friendly": "^0.7.1", "eslint-plugin-cypress": "^2.11.3", diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 385c32cc1..2487b770a 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -6,7 +6,8 @@ "allowJs": true, "noImplicitAny": true, "strict": true, - "module": "commonjs" + "module": "commonjs", + "baseUrl": "http://oauth2proxy.localtest.me:4180/" }, "include": ["./cypress/**/*.ts", "**/*.d.ts"], "exclude": [], diff --git a/local/cypress-jwks-url/keys.json b/local/cypress-jwks-url/keys.json index 4ec30bc87..be9d827af 100644 --- a/local/cypress-jwks-url/keys.json +++ b/local/cypress-jwks-url/keys.json @@ -1 +1 @@ -{"keys":[{"kty":"RSA","kid":"ogzUdyK8H5pjO0-TyEJJjNUFYbRQvmPayNV-Y1xxeQQ","use":"sig","alg":"RS256","e":"AQAB","n":"hT-KVPRendBcTv58Bsi4VGLjzi7NLCp68H8iYXLCBwxcPRbEMLZL8ciXNbjh8q2hLh-9KcHjkQ0cZ21BohHhkIkPV_SJIBC5REp5l6gcZ5QNLMHJWtGYFqb_EsqRz83e8b9aYlBdITGsVsiAQAyPiBDgULCe8p0oL_pKEW6lQEz65UeFX6CIrB17WL3zpbUCYkZ-eFelitdnbGIoSUgOSvG2YaFm_imPqT_3o21RnEV5zlbIWL0LjXyiqvXAj0dojwvVcJZlJt4wNQDcGn-tpgDf3SEUJu1PlOcqQy8ykmH04j85nDsHnzPOjMPugqtSpRWXk3bAOlgqEMMV3q0r9Q"}]} \ No newline at end of file +{"keys":[{"kty":"RSA","kid":"RZ6Oxqp3DjSVwMGrnMI-Z1KSBpdQEMxOTe55m8Lrbes","use":"sig","alg":"RS256","e":"AQAB","n":"0Q60m8-xVDGXz6tJCzDrpftxF88wEByJ8JJY8deJArNso0PTftXKaiLd-Cce4w0AV3N-PoNa_frl_zVDCznanjFB1e3g5GCarTg7-1MshxFiEY7-BVZycLPw2a0Kq_QTH7PW77DYLqOHZ3ENg8pn3W_ya6IRH2cUktrqZ__t-ECbqB-HXXRD1Z8RJRdo1G9cdJAcbMUbCW7CziYkVi9frJnqqgy9V9LfItGu4g2zzj-Cqk-UJHGZbqxLg0ynNQkFp3_T5cSaOC--yZCEtujrSC6OKFsXYWhbqhE2Lqgn_HGHFtttBudXn4I2BopAQu6KeiGCBmHjGsExhWmuzgHpvw"}]} \ No newline at end of file diff --git a/local/feeder-init/idir-user.yaml b/local/feeder-init/idir-user.yaml index e19af7dd0..fdc5ddb46 100644 --- a/local/feeder-init/idir-user.yaml +++ b/local/feeder-init/idir-user.yaml @@ -3,4 +3,5 @@ record: id: 'janis@idir' name: 'Janis Smith' email: 'janis@testmail.com' + provider: 'idir' isAdmin: false diff --git a/local/feeder-init/platform-authz-profile.yaml b/local/feeder-init/platform-authz-profile.yaml index 505d63084..2d48ceda7 100644 --- a/local/feeder-init/platform-authz-profile.yaml +++ b/local/feeder-init/platform-authz-profile.yaml @@ -18,7 +18,7 @@ record: - GatewayConfig.Publish - Content.Publish - Namespace.Assign - owner: janis@idir + owner: janis@testmail.com environmentDetails: - environment: prod issuerUrl: http://keycloak.localtest.me:9080/auth/realms/master diff --git a/local/feeder-init/shared-idp-copy.yaml b/local/feeder-init/shared-idp-copy.yaml new file mode 100644 index 000000000..a2a43881e --- /dev/null +++ b/local/feeder-init/shared-idp-copy.yaml @@ -0,0 +1,20 @@ +entity: CredentialIssuer +record: + id: 'Sample Shared IdP' + namespace: ccplatform + description: 'A Shared IdP for Teams to use' + flow: client-credentials + mode: auto + clientAuthenticator: client-secret + authPlugin: jwt-keycloak + clientRoles: [] + availableScopes: [] + clientMappers: [{"name": "audience","defaultValue": "test2"}] + owner: janis@testmail.com + isShared: true + environmentDetails: + - environment: test + issuerUrl: http://keycloak.localtest.me:9080/auth/realms/master + clientId: gwa-api + clientRegistration: managed + clientSecret: '18900468-3db1-43f7-a8af-e75f079eb742' diff --git a/local/feeder-init/shared-idp.yaml b/local/feeder-init/shared-idp.yaml index e14301ade..db4b928cf 100644 --- a/local/feeder-init/shared-idp.yaml +++ b/local/feeder-init/shared-idp.yaml @@ -9,11 +9,11 @@ record: authPlugin: jwt-keycloak clientRoles: [] availableScopes: [] - owner: janis@idir + owner: janis@testmail.com isShared: true environmentDetails: - - environment: prod + - environment: test issuerUrl: http://keycloak.localtest.me:9080/auth/realms/master clientId: gwa-api clientRegistration: managed - clientSecret: '18900468-3db1-43f7-a8af-e75f079eb742' \ No newline at end of file + clientSecret: '18900468-3db1-43f7-a8af-e75f079eb742' diff --git a/local/keycloak/master-realm.json b/local/keycloak/master-realm.json index 071f1719d..bc8b64447 100644 --- a/local/keycloak/master-realm.json +++ b/local/keycloak/master-realm.json @@ -2565,7 +2565,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-full-name-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-audience-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -3135,4 +3135,4 @@ }, "keycloakVersion" : "11.0.3", "userManagedAccessAllowed" : true -} \ No newline at end of file +} diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 2ea16dbfa..847310919 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -668,7 +668,7 @@ const metadata = { User: { query: 'allUsers', refKey: 'username', - sync: ['name', 'username', 'email', 'legalsAgreed'], + sync: ['name', 'username', 'email', 'legalsAgreed', 'provider'], transformations: { legalsAgreed: { name: 'toStringDefaultArray' }, }, diff --git a/src/services/workflow/validate-active-environment.ts b/src/services/workflow/validate-active-environment.ts index 3c3036f7a..77c28f30a 100644 --- a/src/services/workflow/validate-active-environment.ts +++ b/src/services/workflow/validate-active-environment.ts @@ -175,10 +175,8 @@ export const ValidateActiveEnvironment = async ( const envConfig = getIssuerEnvironmentConfig(issuer, envName); - const isServiceMissingAllPlugins = isServiceMissingAllPluginsHandler( - ['jwt-keycloak'], - (plugin: any) => - plugin.config['well_known_template'].startsWith(envConfig.issuerUrl) + const isServiceMissingAllPlugins = getFuncForMissingJwtKeycloakPlugin( + envConfig.issuerUrl ); // If we are changing the service list, then use that to look for violations, otherwise use what is current @@ -246,10 +244,15 @@ export function isServiceMissingAllPluginsHandler( svc.plugins.filter( (plugin: any) => requiredPlugins.includes(plugin.name) && additionalValidation(plugin) - ).length == requiredPlugins.length; + ).length === requiredPlugins.length; if (serviceLevel) { + logger.debug('Service Level had all plugins'); return false; + } else if (svc.routes.length === 0) { + logger.debug('Service Level missing plugins, and no routes'); + return true; } else { + logger.debug('Service Level missing plugins %d', requiredPlugins.length); // check that at least one route has these plugins return ( svc.routes.filter( @@ -277,3 +280,19 @@ async function getNamespaceOrganizationDetails( return await svc.getNamespaceOrganizationDetails(ns); } + +export function getFuncForMissingJwtKeycloakPlugin(issuerUrl: string) { + const isWellKnownUndefined = (url: string) => { + return ( + Boolean(url) == false || url === '%s/.well-known/openid-configuration' + ); + }; + + return isServiceMissingAllPluginsHandler( + ['jwt-keycloak'], + (plugin: any) => + plugin.config['well_known_template']?.startsWith(issuerUrl) || + (isWellKnownUndefined(plugin.config['well_known_template']) && + plugin.config['allowed_iss']?.includes(issuerUrl)) + ); +} diff --git a/src/test/integrated/workflow/validate-environment.ts b/src/test/integrated/workflow/validate-environment.ts index 2ddb00dba..96894eca6 100644 --- a/src/test/integrated/workflow/validate-environment.ts +++ b/src/test/integrated/workflow/validate-environment.ts @@ -9,6 +9,8 @@ node dist/test/integrated/workflow/validate-environment.js import InitKeystone from '../keystonejs/init'; import { o } from '../util'; import { ValidateActiveEnvironment } from '../../../services/workflow'; +import { getFuncForMissingJwtKeycloakPlugin } from '../../../services/workflow/validate-active-environment'; +import { strict as assert } from 'assert'; (async () => { const keystone = await InitKeystone(); @@ -58,7 +60,7 @@ import { ValidateActiveEnvironment } from '../../../services/workflow'; // await new Promise((r) => setTimeout(r, 10000)); } - if (true) { + if (false) { const operation = 'create'; const existingItem = null as any; const originalInput = { @@ -86,5 +88,22 @@ import { ValidateActiveEnvironment } from '../../../services/workflow'; // await new Promise((r) => setTimeout(r, 10000)); } + if (true) { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: null, + allowed_iss: ['https://auth.local/realms/myrealm'], + }, + }, + ], + routes: [], + } as any); + assert.strictEqual(test, false); + } + await keystone.disconnect(); })(); diff --git a/src/test/services/workflow/validate-active-environment.test.js b/src/test/services/workflow/validate-active-environment.test.js index 46accccb2..d02ca3be7 100644 --- a/src/test/services/workflow/validate-active-environment.test.js +++ b/src/test/services/workflow/validate-active-environment.test.js @@ -4,6 +4,7 @@ import { ValidateActiveEnvironment, isServiceMissingAllPluginsHandler, } from '../../../services/workflow'; +import { getFuncForMissingJwtKeycloakPlugin } from '../../../services/workflow/validate-active-environment'; import { json } from './utils'; @@ -137,6 +138,144 @@ describe('Validate Active Environment', function () { const result = isServiceMissingAllPluginsHandler(['acl'])(svc); expect(result).toBe(false); }); + + it('service plugin is valid - good allowed_iss', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: null, + allowed_iss: ['https://auth.local/realms/myrealm'], + }, + }, + ], + routes: [], + }); + expect(test).toBe(false); + }); + + it('service plugin is valid - good well known', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: + 'https://auth.local/realms/myrealm/.well-known/openid-configuration', + allowed_iss: [], + }, + }, + ], + routes: [], + }); + expect(test).toBe(false); + }); + + it('service route plugin is valid - good well known', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [], + routes: [ + { + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: + 'https://auth.local/realms/myrealm/.well-known/openid-configuration', + allowed_iss: [], + }, + }, + ], + }, + ], + }); + expect(test).toBe(false); + }); + + it('service plugin is valid - well known default', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: '%s/.well-known/openid-configuration', + allowed_iss: ['https://auth.local/realms/myrealm'], + }, + }, + ], + routes: [], + }); + expect(test).toBe(false); + }); + + it('service plugin is invalid - different realm', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: null, + allowed_iss: ['https://auth.local/realms/anotherrealm'], + }, + }, + ], + routes: [], + }); + expect(test).toBe(true); + }); + + it('service plugin is invalid - no well known', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: null, + }, + }, + ], + routes: [], + }); + expect(test).toBe(true); + }); + + it('service plugin is invalid - missing plugin', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'acl', + config: {}, + }, + ], + routes: [], + }); + expect(test).toBe(true); + }); + + it('service plugin is invalid - well known mismatch', function () { + const issuerUrl = 'https://auth.local/realms/myrealm'; + const test = getFuncForMissingJwtKeycloakPlugin(issuerUrl)({ + plugins: [ + { + name: 'jwt-keycloak', + config: { + well_known_template: 'https://blahblah', + allowed_iss: ['https://auth.local/realms/myrealm'], + }, + }, + ], + routes: [], + }); + expect(test).toBe(true); + }); }); /* From 569cb9a739d8016b3a0e71bb46e04548797683fb Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Mon, 13 Feb 2023 12:43:00 -0800 Subject: [PATCH 2/8] Fix org tooltip rendering logic (#739) --- src/nextapp/pages/manager/namespaces/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index a651a4796..0a05dffbe 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -195,7 +195,7 @@ const NamespacesPage: React.FC = () => { {currentOrg.text} - {user?.roles.includes('api-owner') && ( + {currentOrg.assigned && ( From 07ecdfa7a6323e1302e73c5e52c675aeb8df094d Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Mon, 13 Feb 2023 12:47:03 -0800 Subject: [PATCH 3/8] Sprint 63 UI Fixes (#743) * Load the consumers table first before access requests. * Fix auth-code preventing environment edit. --- src/mocks/resolvers/consumers.js | 6 +++++- .../environment-config/credential-issuer-select.tsx | 6 +++--- .../environment-config/environment-config.tsx | 3 +-- src/nextapp/pages/manager/consumers/index.tsx | 10 ++++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/mocks/resolvers/consumers.js b/src/mocks/resolvers/consumers.js index 11bb587da..629faf08a 100644 --- a/src/mocks/resolvers/consumers.js +++ b/src/mocks/resolvers/consumers.js @@ -216,7 +216,10 @@ class Store { export const store = new Store(consumers); export const getConsumersHandler = (req, res, ctx) => { - return res(ctx.delay(), ctx.data({ ...store.data, allConsumerGroupLabels })); + return res( + ctx.delay(2000), + ctx.data({ ...store.data, allConsumerGroupLabels }) + ); }; export const getConsumerHandler = (req, res, ctx) => { @@ -325,6 +328,7 @@ export const getConsumerHandler = (req, res, ctx) => { export const getAccessRequestsHandler = (req, res, ctx) => { return res( + ctx.delay(1000), ctx.data({ allAccessRequestsByNamespace: store.data.allAccessRequestsByNamespace, }) diff --git a/src/nextapp/components/environment-config/credential-issuer-select.tsx b/src/nextapp/components/environment-config/credential-issuer-select.tsx index c57cafa4e..3a04cc92d 100644 --- a/src/nextapp/components/environment-config/credential-issuer-select.tsx +++ b/src/nextapp/components/environment-config/credential-issuer-select.tsx @@ -14,6 +14,7 @@ const CredentialIssuerSelect: React.FC = ({ onChange, value, }) => { + const isEnabled = flow !== 'client-credentials'; const { data, isLoading, isError } = useApi( ['environment-credential-users', flow], { @@ -22,6 +23,7 @@ const CredentialIssuerSelect: React.FC = ({ }, { suspense: false, + enabled: isEnabled, } ); @@ -29,9 +31,7 @@ const CredentialIssuerSelect: React.FC = ({ - {(flow === 'client-credentials' || - flow === 'authorization-code') && ( + {flow === 'client-credentials' && ( } /> - + {isSuccess && ( + + )} Date: Mon, 13 Feb 2023 12:51:51 -0800 Subject: [PATCH 4/8] Feature/ns access UI (#733) Co-authored-by: Joshua Jones --- .github/workflows/ci-build-deploy.yaml | 4 +- .../platform-authz-profile-shared.yaml | 18 ++ .../feeder-init/platform-authz-profile.yaml | 2 +- .github/workflows/scripts/init.sh | 4 + .../httplocalhost3000admingraphiql-710ac3.gql | 12 +- .../httplocalhost3000admingraphiql-7e4a9a.gql | 6 + .../httplocalhost3000admingraphiql-ca2482.gql | 6 + ...lhost4180managernamespaceaccess-1fc5e5.gql | 6 + ...lhost4180managernamespaceaccess-33a029.gql | 14 ++ ...lhost4180managernamespaceaccess-411eb0.gql | 19 ++ ...lhost4180managernamespaceaccess-a53363.gql | 6 + ...lhost4180managernamespaceaccess-eeebd7.gql | 14 ++ src/authz/matrix.csv | 3 +- src/controllers/v2/openapi.yaml | 17 +- src/controllers/v2/routes.ts | 11 +- src/initial-data.js | 34 +-- src/lists/extensions/UMAPermissionTicket.ts | 145 +++++-------- src/lists/extensions/UMAPolicy.ts | 101 +++++---- src/mocks/handlers.js | 4 + src/mocks/resolvers/namespace-access.js | 65 ++++-- src/mocks/resolvers/personas.js | 62 +++--- .../namespace-access-dialog.tsx | 113 ++++++++-- .../service-accounts-access.tsx | 79 ++++++- .../components/namespace-access/types.ts | 13 ++ .../namespace-access/users-access.tsx | 109 ++++++---- src/nextapp/shared/theme.ts | 3 + src/nextapp/shared/types/query.types.ts | 16 ++ src/server.ts | 5 +- .../keycloak/permission-ticket-service.ts | 1 + src/services/keystone/types.ts | 16 ++ src/services/uma2/policy-service.ts | 16 ++ src/services/workflow/namespace-activity.ts | 2 +- src/services/workflow/ns-uma-perm-access.ts | 195 ++++++++++++++++++ src/services/workflow/ns-uma-policy-access.ts | 122 +++++++++++ src/test/integrated/uma2/permissionTicket.ts | 81 ++++++++ 35 files changed, 1035 insertions(+), 289 deletions(-) create mode 100644 .github/workflows/scripts/feeder-init/platform-authz-profile-shared.yaml create mode 100644 src/authz/graphql-whitelist/httplocalhost3000admingraphiql-7e4a9a.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost3000admingraphiql-ca2482.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-1fc5e5.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-33a029.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-411eb0.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-a53363.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-eeebd7.gql create mode 100644 src/nextapp/components/namespace-access/types.ts create mode 100644 src/services/workflow/ns-uma-perm-access.ts create mode 100644 src/services/workflow/ns-uma-policy-access.ts create mode 100644 src/test/integrated/uma2/permissionTicket.ts diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index 4a7aa53ad..cd5384866 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -162,8 +162,8 @@ jobs: replicaCount: 1 rollingUpdate: - maxUnavailable: 50% - maxSurge: 50% + maxUnavailable: 100% + maxSurge: 0% image: repository: ${{ env.REGISTRY }}/bcgov/api-services-portal/api-services-portal diff --git a/.github/workflows/scripts/feeder-init/platform-authz-profile-shared.yaml b/.github/workflows/scripts/feeder-init/platform-authz-profile-shared.yaml new file mode 100644 index 000000000..a38b10f45 --- /dev/null +++ b/.github/workflows/scripts/feeder-init/platform-authz-profile-shared.yaml @@ -0,0 +1,18 @@ +entity: CredentialIssuer +record: + id: 'Shared IdP' + namespace: platform + description: 'Shared IdP Authorization Profile' + flow: client-credentials + mode: auto + clientAuthenticator: client-secret + authPlugin: jwt-keycloak + clientRoles: [] + owner: api-owner@local + isShared: true + environmentDetails: + - environment: prod + issuerUrl: '{OIDC_ISSUER}' + clientId: '{OIDC_CLIENT_ID}' + clientSecret: '{OIDC_CLIENT_SECRET}' + clientRegistration: managed diff --git a/.github/workflows/scripts/feeder-init/platform-authz-profile.yaml b/.github/workflows/scripts/feeder-init/platform-authz-profile.yaml index db6272aff..dbf523ad5 100644 --- a/.github/workflows/scripts/feeder-init/platform-authz-profile.yaml +++ b/.github/workflows/scripts/feeder-init/platform-authz-profile.yaml @@ -18,7 +18,7 @@ record: - GatewayConfig.Publish - Content.Publish - CredentialIssuer.Admin - owner: user_api-owner + owner: api-owner@local environmentDetails: - environment: prod issuerUrl: '{OIDC_ISSUER}' diff --git a/.github/workflows/scripts/init.sh b/.github/workflows/scripts/init.sh index c8036ff13..88a65c97c 100755 --- a/.github/workflows/scripts/init.sh +++ b/.github/workflows/scripts/init.sh @@ -2,9 +2,12 @@ python scripts/template.py scripts/feeder-init/legal.yaml legal.yaml python scripts/template.py scripts/feeder-init/platform-authz-profile.yaml platform-authz-profile.yaml +python scripts/template.py scripts/feeder-init/platform-authz-profile-shared.yaml platform-authz-profile-shared.yaml python scripts/template.py scripts/feeder-init/platform-dataset.yaml platform-dataset.yaml python scripts/template.py scripts/feeder-init/platform-gwa-api.yaml platform-gwa-api.yaml +sleep 15 + while true; do status=$(curl -o /dev/null -Isw '%{http_code}\n' ${PORTAL_URL}/health) echo "$status" @@ -16,6 +19,7 @@ while true; do sleep 5 curl --fail -v http://localhost:8080/push -F yaml=@legal.yaml curl --fail -v http://localhost:8080/push -F yaml=@platform-authz-profile.yaml + curl --fail -v http://localhost:8080/push -F yaml=@platform-authz-profile-shared.yaml curl --fail -v http://localhost:8080/push -F yaml=@platform-dataset.yaml curl --fail -v http://localhost:8080/push -F yaml=@platform-gwa-api.yaml curl --fail -v http://localhost:8080/push -F yaml=@scripts/feeder-init/organization-unit.yaml diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-710ac3.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-710ac3.gql index 01d59efb2..dfb8303e5 100644 --- a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-710ac3.gql +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-710ac3.gql @@ -1,14 +1,6 @@ - mutation GrantSAAccess( - $prodEnvId: ID! - $resourceId: ID! - $data: UMAPolicyInput! - ) { - createUmaPolicy( - prodEnvId: $prodEnvId - resourceId: $resourceId - data: $data - ) { + mutation GrantSAAccess($prodEnvId: ID!, $resourceId: String!, $data: UMAPolicyInput!) { + createUmaPolicy(prodEnvId: $prodEnvId, resourceId: $resourceId, data: $data) { id } } diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-7e4a9a.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-7e4a9a.gql new file mode 100644 index 000000000..b834fefe1 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-7e4a9a.gql @@ -0,0 +1,6 @@ + + mutation UpdateSAAccess($prodEnvId: ID!, $resourceId: String!, $data: UMAPolicyInput!) { + updateUmaPolicy(prodEnvId: $prodEnvId, resourceId: $resourceId, data: $data) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-ca2482.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-ca2482.gql new file mode 100644 index 000000000..0ee16f66f --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-ca2482.gql @@ -0,0 +1,6 @@ + + mutation UpdateUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) { + updatePermissions(prodEnvId: $prodEnvId, data: $data) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-1fc5e5.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-1fc5e5.gql new file mode 100644 index 000000000..39be996f2 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-1fc5e5.gql @@ -0,0 +1,6 @@ + + mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) { + updatePermissions(prodEnvId: $prodEnvId, data: $data) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-33a029.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-33a029.gql new file mode 100644 index 000000000..88e03936a --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-33a029.gql @@ -0,0 +1,14 @@ + + mutation UpdateSAAccess( + $prodEnvId: ID! + $resourceId: String! + $data: UMAPolicyInput! + ) { + updateUmaPolicy( + prodEnvId: $prodEnvId + resourceId: $resourceId + data: $data + ) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-411eb0.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-411eb0.gql new file mode 100644 index 000000000..37d08efe4 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-411eb0.gql @@ -0,0 +1,19 @@ + + query GetUserPermissions($resourceId: String!, $prodEnvId: ID!) { + getPermissionTicketsForResource( + prodEnvId: $prodEnvId + resourceId: $resourceId + ) { + id + owner + ownerName + requester + requesterName + requesterEmail + resource + resourceName + scope + scopeName + granted + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-a53363.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-a53363.gql new file mode 100644 index 000000000..0ee16f66f --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-a53363.gql @@ -0,0 +1,6 @@ + + mutation UpdateUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) { + updatePermissions(prodEnvId: $prodEnvId, data: $data) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-eeebd7.gql b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-eeebd7.gql new file mode 100644 index 000000000..044f39967 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managernamespaceaccess-eeebd7.gql @@ -0,0 +1,14 @@ + + mutation GrantSAAccess( + $prodEnvId: ID! + $resourceId: String! + $data: UMAPolicyInput! + ) { + updateUmaPolicy( + prodEnvId: $prodEnvId + resourceId: $resourceId + data: $data + ) { + id + } + } diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index d370c7956..80c827723 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -127,12 +127,13 @@ API Owner Role Rules,,forceDeleteNamespace,,,,,,,api-owner,,,allow, API Owner Role Rules,,namespace,,,,,,,api-owner,,,allow, API Owner Role Rules,,currentNamespace,,,,,,,,,"portal-user,api-owner,provider-user,access-manager,credential-admin",allow, API Owner / Provider Role Rules,,updateCurrentNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updatePermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, API Owner Role Rules,,allResourceSets,,,,,,,api-owner,,,allow, API Owner Role Rules,,getUmaPoliciesForResource,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateUmaPolicy,,,,,,,api-owner,,,allow, API Owner Role Rules,,createUmaPolicy,,,,,,,api-owner,,,allow, API Owner Role Rules,,deleteUmaPolicy,,,,,,,api-owner,,,allow, API Owner Role Rules,,getServiceAccounts,,,,,,,,,"api-owner,provider-user",allow, diff --git a/src/controllers/v2/openapi.yaml b/src/controllers/v2/openapi.yaml index d1cd4998e..5c6ad39cd 100644 --- a/src/controllers/v2/openapi.yaml +++ b/src/controllers/v2/openapi.yaml @@ -351,6 +351,9 @@ components: mode: auto environmentDetails: [] owner: acope@idir + Maybe_Scalars-at-String_: + type: string + nullable: true UmaScope: properties: name: @@ -368,8 +371,10 @@ components: - $ref: '#/components/schemas/UmaScope' nullable: true - Maybe_Scalars-at-String_: - type: string + Maybe_Array_Maybe_UmaScope___: + items: + $ref: '#/components/schemas/Maybe_UmaScope_' + type: array nullable: true Maybe_Array_Maybe_Scalars-at-String___: items: @@ -408,22 +413,18 @@ components: prodEnvId: $ref: '#/components/schemas/Maybe_Scalars-at-String_' scopes: - items: - $ref: '#/components/schemas/Maybe_UmaScope_' - type: array + $ref: '#/components/schemas/Maybe_Array_Maybe_UmaScope___' name: type: string id: - type: string + $ref: '#/components/schemas/Maybe_Scalars-at-String_' __typename: type: string enum: - Namespace nullable: false required: - - scopes - name - - id type: object ActivityDetail: properties: diff --git a/src/controllers/v2/routes.ts b/src/controllers/v2/routes.ts index 8673674b9..21bb4b1ec 100644 --- a/src/controllers/v2/routes.ts +++ b/src/controllers/v2/routes.ts @@ -221,6 +221,11 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "Maybe_Scalars-at-String_": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "UmaScope": { "dataType": "refAlias", "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"__typename":{"dataType":"enum","enums":["UMAScope"]}},"validators":{}}, @@ -231,9 +236,9 @@ const models: TsoaRoute.Models = { "type": {"dataType":"union","subSchemas":[{"ref":"UmaScope"},{"dataType":"enum","enums":[null]}],"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "Maybe_Scalars-at-String_": { + "Maybe_Array_Maybe_UmaScope___": { "dataType": "refAlias", - "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"validators":{}}, + "type": {"dataType":"union","subSchemas":[{"dataType":"array","array":{"dataType":"refAlias","ref":"Maybe_UmaScope_"}},{"dataType":"enum","enums":[null]}],"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Maybe_Array_Maybe_Scalars-at-String___": { @@ -258,7 +263,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Namespace": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"orgAdmins":{"ref":"Maybe_Array_Maybe_Scalars-at-String___"},"orgNoticeViewed":{"ref":"Maybe_Scalars-at-Boolean_"},"orgEnabled":{"ref":"Maybe_Scalars-at-Boolean_"},"orgUpdatedAt":{"ref":"Maybe_Scalars-at-Float_"},"orgUnit":{"ref":"Maybe_Scalars-at-JSON_"},"org":{"ref":"Maybe_Scalars-at-JSON_"},"permProtectedNs":{"ref":"Maybe_Scalars-at-String_"},"permDataPlane":{"ref":"Maybe_Scalars-at-String_"},"permDomains":{"ref":"Maybe_Array_Maybe_Scalars-at-String___"},"prodEnvId":{"ref":"Maybe_Scalars-at-String_"},"scopes":{"dataType":"array","array":{"dataType":"refAlias","ref":"Maybe_UmaScope_"},"required":true},"name":{"dataType":"string","required":true},"id":{"dataType":"string","required":true},"__typename":{"dataType":"enum","enums":["Namespace"]}},"validators":{}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"orgAdmins":{"ref":"Maybe_Array_Maybe_Scalars-at-String___"},"orgNoticeViewed":{"ref":"Maybe_Scalars-at-Boolean_"},"orgEnabled":{"ref":"Maybe_Scalars-at-Boolean_"},"orgUpdatedAt":{"ref":"Maybe_Scalars-at-Float_"},"orgUnit":{"ref":"Maybe_Scalars-at-JSON_"},"org":{"ref":"Maybe_Scalars-at-JSON_"},"permProtectedNs":{"ref":"Maybe_Scalars-at-String_"},"permDataPlane":{"ref":"Maybe_Scalars-at-String_"},"permDomains":{"ref":"Maybe_Array_Maybe_Scalars-at-String___"},"prodEnvId":{"ref":"Maybe_Scalars-at-String_"},"scopes":{"ref":"Maybe_Array_Maybe_UmaScope___"},"name":{"dataType":"string","required":true},"id":{"ref":"Maybe_Scalars-at-String_"},"__typename":{"dataType":"enum","enums":["Namespace"]}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ActivityDetail": { diff --git a/src/initial-data.js b/src/initial-data.js index cec483ef4..11593ec6b 100644 --- a/src/initial-data.js +++ b/src/initial-data.js @@ -1,7 +1,7 @@ const crypto = require('crypto'); const randomString = () => crypto.randomBytes(6).hexSlice(); -module.exports = async keystone => { +module.exports = async (keystone) => { // Count existing users const { data: { @@ -19,7 +19,7 @@ module.exports = async keystone => { if (count === 0) { const password = randomString(); const email = 'admin@local'; - const roles = ['developer'] + const roles = ['developer']; const { errors } = await keystone.executeGraphQL({ context: keystone.createContext({ skipAccessControl: true }), @@ -45,21 +45,27 @@ module.exports = async keystone => { } } - for (role of [ 'developer', 'api-owner', 'api-manager', 'credential-admin', 'aps-admin']) { - const password = "password"; - const name = "user_" + role; - const email = role + "@local"; - const roles = JSON.stringify([role]) + for (role of [ + 'developer', + 'api-owner', + 'api-manager', + 'credential-admin', + 'aps-admin', + ]) { + const password = 'password'; + const name = 'user_' + role; + const email = role + '@local'; + const roles = JSON.stringify([role]); + const provider = 'idir'; const { errors } = await keystone.executeGraphQL({ - context: keystone.createContext({ skipAccessControl: true }), - query: `mutation roleUser($name: String, $password: String, $email: String, $roles: String) { - createUser(data: {name: $name, username: $name, email: $email, isAdmin: false, password: $password, roles: $roles}) { + context: keystone.createContext({ skipAccessControl: true }), + query: `mutation roleUser($name: String, $password: String, $email: String, $roles: String, $provider: String) { + createUser(data: {name: $name, username: $name, email: $email, isAdmin: false, password: $password, provider: $provider, roles: $roles}) { id } }`, - variables: { name, password, email, roles }, - }); - } + variables: { name, password, email, roles, provider }, + }); + } }; - diff --git a/src/lists/extensions/UMAPermissionTicket.ts b/src/lists/extensions/UMAPermissionTicket.ts index 3197125cf..06d6bd5ec 100644 --- a/src/lists/extensions/UMAPermissionTicket.ts +++ b/src/lists/extensions/UMAPermissionTicket.ts @@ -10,6 +10,10 @@ import { strict as assert } from 'assert'; import { Logger } from '../../logger'; import { StructuredActivityService } from '../../services/workflow'; import { lookupUsersByUsernames, switchTo } from '../../services/keystone'; +import { + revokePermissions, + updatePermissions, +} from '../../services/workflow/ns-uma-perm-access'; const logger = Logger('lists.umaticket'); @@ -22,6 +26,7 @@ type UMAPermissionTicket { resourceName: String!, requester: String!, requesterName: String!, + requesterEmail: String, owner: String!, ownerName: String!, granted: Boolean! @@ -126,6 +131,7 @@ module.exports = { .filter((u) => u.username == perm.requesterName) .pop(); perm.requesterName = user?.name || perm.requesterName; + perm.requesterEmail = user?.email; }); return permissions; }, @@ -143,71 +149,62 @@ module.exports = { info: any, { query, access }: any ) => { - const { email, scopes, resourceId } = args.data; const envCtx = await getEnvironmentContext( context, args.prodEnvId, access ); - const resourceIds = await getResourceSets(envCtx); - assert.strictEqual( - resourceIds.filter((rid) => rid === resourceId).length, - 1, - 'Invalid Resource' - ); + const { email, scopes, resourceId } = args.data; - const userApi = new KeycloakUserService(envCtx.openid.issuer); - await userApi.login( - envCtx.issuerEnvConfig.clientId, - envCtx.issuerEnvConfig.clientSecret - ); - const user = await userApi.lookupUserByEmail( - args.data.email, - false, - ['idir'] + const { result } = await updatePermissions( + context, + envCtx, + email, + scopes, + resourceId, + 'grant' ); - const displayName = - userApi.getOneAttributeValue(user, 'display_name') || - user.email; - const result = []; - const granted = - 'granted' in args.data ? args.data['granted'] : true; - const permissionApi = new KeycloakPermissionTicketService( - envCtx.openid.issuer, - envCtx.accessToken + return result; + }, + access: EnforcementPoint, + }, + { + schema: + 'updatePermissions(prodEnvId: ID!, data: UMAPermissionTicketInput! ): [UMAPermissionTicket]', + resolver: async ( + item: any, + args: any, + context: any, + info: any, + { query, access }: any + ) => { + const envCtx = await getEnvironmentContext( + context, + args.prodEnvId, + access ); - for (const scope of scopes) { - const permission = await permissionApi.createOrUpdatePermission( - resourceId, - user.id, - granted, - scope - ); - result.push({ id: permission.id }); - } - await new StructuredActivityService( - context.sudo(), - context.authedItem['namespace'] - ).logNamespaceAccess( - true, - 'granted', - 'namespace access', - 'user', - displayName, - scopes + const { email, scopes, resourceId } = args.data; + + const { userId, result } = await updatePermissions( + context, + envCtx, + email, + scopes, + resourceId, + 'update' ); // refresh the permissions for this user in TemporaryIdentity try { logger.info( 'User matching %s with %j', - user.id, + userId, context.req.user ); - if (user.id === context.req.user.sub) { + if (userId === context.req.user.sub) { const subjectToken = context.req.headers['x-forwarded-access-token']; @@ -220,7 +217,9 @@ module.exports = { context.req.user.provider ); } - } catch (err) {} + } catch (err) { + logger.warn('[updatePermissions] switch failed %s', err); + } return result; }, @@ -242,55 +241,15 @@ module.exports = { access ); - const resourceIds = await getResourceSets(envCtx); - assert.strictEqual( - resourceIds.filter((rid) => rid === args.resourceId).length, - 1, - 'Invalid Resource' - ); - - const permissionApi = new KeycloakPermissionTicketService( - envCtx.openid.issuer, - envCtx.accessToken - ); - - const perms = await permissionApi.listPermissions({ - resourceId: args.resourceId, - returnNames: true, - }); + const { resourceId, ids } = args; - const requesterIds = []; - const deletedScopes = []; - for (const permId of args.ids) { - const foundPerms = perms.filter((perm) => perm.id === permId); - assert.strictEqual(foundPerms.length, 1, 'Invalid Permission'); - deletedScopes.push(foundPerms[0].scopeName); - requesterIds.push(foundPerms[0].requester); - await permissionApi.deletePermission(permId); - } - - const userApi = new KeycloakUserService(envCtx.openid.issuer); - await userApi.login( - envCtx.issuerEnvConfig.clientId, - envCtx.issuerEnvConfig.clientSecret - ); - const user = await userApi.lookupUserById(requesterIds.pop()); - const displayName = user.attributes.display_name || user.email; - - await new StructuredActivityService( - context.sudo(), - context.authedItem['namespace'] - ).logNamespaceAccess( - true, - 'revoked', - 'namespace access', - 'user', - displayName, - deletedScopes + const { userId } = await revokePermissions( + context, + envCtx, + resourceId, + ids ); - logger.warn('[revokePermissions] %j', perms); - return true; }, access: EnforcementPoint, diff --git a/src/lists/extensions/UMAPolicy.ts b/src/lists/extensions/UMAPolicy.ts index e5c303ab3..1e1224998 100644 --- a/src/lists/extensions/UMAPolicy.ts +++ b/src/lists/extensions/UMAPolicy.ts @@ -4,6 +4,13 @@ import { getEnvironmentContext, getResourceSets } from './Common'; import { strict as assert } from 'assert'; import { Logger } from '../../logger'; import { StructuredActivityService } from '../../services/workflow'; +import { + createUmaPolicy, + updateUmaPolicy, + revokeUmaPolicy, +} from '../../services/workflow/ns-uma-policy-access'; +import PolicyRepresentation from '@keycloak/keycloak-admin-client/lib/defs/policyRepresentation'; +import { UmaPolicyInput } from '../../services/keystone/types'; const logger = Logger('lists.umapolicy'); @@ -91,43 +98,26 @@ module.exports = { access ); - const resourceIds = await getResourceSets(envCtx); - assert.strictEqual( - resourceIds.filter((rid) => rid === args.resourceId).length, - 1, - 'Invalid Resource' - ); + const input: UmaPolicyInput = args.data; - const policyApi = new UMAPolicyService( - envCtx.uma2.policy_endpoint, - envCtx.accessToken - ); - - // name, scopes - const umaPolicy = await policyApi.createUmaPolicy( + const umaPolicy: Policy = { + name: input.name, + description: `Service Acct ${input.name}`, + clients: [input.name], + scopes: args.data.scopes, + }; + return await createUmaPolicy( + context, + envCtx, args.resourceId, - args.data as Policy + umaPolicy ); - - await new StructuredActivityService( - context.sudo(), - context.authedItem['namespace'] - ).logNamespaceAccess( - true, - 'granted', - 'namespace access', - 'client', - args.data.name, - args.data.scopes - ); - - return umaPolicy; }, access: EnforcementPoint, }, { schema: - 'deleteUmaPolicy(prodEnvId: ID!, resourceId: String!, policyId: String!): Boolean', + 'updateUmaPolicy(prodEnvId: ID!, resourceId: String!, data: UMAPolicyInput!): UMAPolicy', resolver: async ( item: any, args: any, @@ -141,37 +131,40 @@ module.exports = { access ); - const resourceIds = await getResourceSets(envCtx); - assert.strictEqual( - resourceIds.filter((rid) => rid === args.resourceId).length, - 1, - 'Invalid Resource' - ); + const input: UmaPolicyInput = args.data; - const policyApi = new UMAPolicyService( - envCtx.uma2.policy_endpoint, - envCtx.accessToken + return await updateUmaPolicy( + context, + envCtx, + args.resourceId, + input.name, + input.scopes + ); + }, + access: EnforcementPoint, + }, + { + schema: + 'deleteUmaPolicy(prodEnvId: ID!, resourceId: String!, policyId: String!): Boolean', + resolver: async ( + item: any, + args: any, + context: any, + info: any, + { query, access }: any + ) => { + const envCtx = await getEnvironmentContext( + context, + args.prodEnvId, + access ); - const policy = await policyApi.findPolicyByResource( + await revokeUmaPolicy( + context, + envCtx, args.resourceId, args.policyId ); - logger.warn('Policy %j', policy); - - await policyApi.deleteUmaPolicy(args.policyId); - - await new StructuredActivityService( - context.sudo(), - context.authedItem['namespace'] - ).logNamespaceAccess( - true, - 'revoked', - 'namespace access', - 'client', - policy.name, - policy.scopes - ); return true; }, diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index 68ba8c4b3..2e5256b3e 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -42,7 +42,9 @@ import { getServiceAccessPermissionsHandler, getUserPermissionsHandler, grantAccessHandler, + updateAccessHandler, grantSAAccessHandler, + updateSAAccessHandler, revokeAccessHandler, revokeSAAccessHandler, getListOrganizationsHandler, @@ -272,7 +274,9 @@ export const handlers = [ keystone.query('ListOrganizationUnits', getListOrganizationUnitsHandler), keystone.mutation('UpdateCurrentNamespace', updateCurrentNamesSpaceHandler), keystone.mutation('GrantUserAccess', grantAccessHandler), + keystone.mutation('UpdateUserAccess', updateAccessHandler), keystone.mutation('GrantSAAccess', grantSAAccessHandler), + keystone.mutation('UpdateSAAccess', updateSAAccessHandler), keystone.mutation('RevokeAccess', revokeAccessHandler), keystone.mutation('RevokeSAAccess', revokeSAAccessHandler), // MUTATIONS diff --git a/src/mocks/resolvers/namespace-access.js b/src/mocks/resolvers/namespace-access.js index 0ef50edf1..b3828a257 100644 --- a/src/mocks/resolvers/namespace-access.js +++ b/src/mocks/resolvers/namespace-access.js @@ -1,4 +1,5 @@ import { shuffle } from 'lodash'; +import casual from 'casual-browserify'; let permissions = [ { @@ -7,6 +8,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'wolfeschlegelsteinhausen@idir', + requesterEmail: 'wolfeschlegelsteinhausen@domain.com', resource: 'r1', resourceName: 'aps-portal', scope: 's1', @@ -19,6 +21,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'wolfeschlegelsteinhausen@idir', + requesterEmail: 'wolfeschlegelsteinhausen@domain.com', resource: 'r1', resourceName: 'aps-portal', scope: 's2', @@ -31,6 +34,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'wolfeschlegelsteinhausen@idir', + requesterEmail: 'wolfeschlegelsteinhausen@domain.com', resource: 'r1', resourceName: 'aps-portal', scope: 's3', @@ -43,6 +47,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'wolfeschlegelsteinhausen@idir', + requesterEmail: 'wolfeschlegelsteinhausen@domain.com', resource: 'r1', resourceName: 'aps-portal', scope: 's5', @@ -55,6 +60,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'wolfeschlegelsteinhausen@idir', + requesterEmail: 'wolfeschlegelsteinhausen@domain.com', resource: 'r1', resourceName: 'aps-portal', scope: 's1', @@ -67,6 +73,7 @@ let permissions = [ ownerName: 'aps', requester: '123', requesterName: 'elischen@idir', + requesterEmail: 'elischen@domain.com', resource: 'r2', resourceName: 'aps-portal', scope: 's4', @@ -495,7 +502,14 @@ export const getResourceSetHandler = (_, res, ctx) => { }; export const grantAccessHandler = (req, res, ctx) => { - if (req.variables.data.username === 'fail') { + const { email, scopes } = req.variables.data; + const names = new Map([ + ['elischen@domain.com', 'elischen@idir'], + ['wolfeschlegelsteinhausen@domain.com', 'wolfeschlegelsteinhausen@idir'], + ['harley@gov.ca', 'harley@idir'], + ]); + + if (!names.has(email)) { return res( ctx.data({ errors: [ @@ -521,20 +535,37 @@ export const grantAccessHandler = (req, res, ctx) => { }) ); } - req.variables.data.scopes.forEach((scope) => { - permissions.push({ - id: `perm${permissions.length + 1}`, - owner: 'o1', - ownerName: 'aps', - requester: '123', - requesterName: req.variables.data.username, - resource: 'r1', - resourceName: 'aps-portal', - scope: 's1', - scopeName: scope, - granted: false, + + if (permissions.some((p) => p.requesterEmail === email)) { + permissions = permissions.filter((p) => { + if (p.requesterEmail === email) { + return scopes.includes(p.scope); + } + return true; }); + } + + scopes.forEach((scope) => { + const existingScope = permissions.findIndex( + (p) => p.requesterEmail === email && p.scope === scope + ); + if (existingScope < 0) { + permissions.push({ + id: `perm${permissions.length + 1}`, + owner: 'o1', + ownerName: 'aps', + requester: '123', + requesterName: names.get(email), + requesterEmail: email, + resource: 'r1', + resourceName: 'aps-portal', + scope: 's1', + scopeName: scope, + granted: false, + }); + } }); + return res( ctx.data({ id: 'a4', @@ -542,6 +573,11 @@ export const grantAccessHandler = (req, res, ctx) => { ); }; +export const updateAccessHandler = (req, res, ctx) => { + console.log(req.variables); + return res(ctx.data({})); +}; + export const grantSAAccessHandler = (req, res, ctx) => { if (req.variables.data.name === 'fail') { return res( @@ -589,6 +625,9 @@ export const grantSAAccessHandler = (req, res, ctx) => { ); }; +export const updateSAAccessHandler = (req, res, ctx) => { + return res(ctx.data({})); +}; export const revokeAccessHandler = (req, res, ctx) => { const { tickets } = req.variables; permissions = permissions.filter((p) => !tickets.includes(p.scope)); diff --git a/src/mocks/resolvers/personas.js b/src/mocks/resolvers/personas.js index a5d5df962..92a39109f 100644 --- a/src/mocks/resolvers/personas.js +++ b/src/mocks/resolvers/personas.js @@ -1,38 +1,38 @@ export const harleyBusiness = { - legalName: "Easy Drug Mart", - address: { - addressLine1: "51, W Broadway", - addressLine2: "", - city: "Vancouver", - postal: "V8T 1E7", - province: "BC", - country: "Canada", - }, + legalName: 'Easy Drug Mart', + address: { + addressLine1: '51, W Broadway', + addressLine2: '', + city: 'Vancouver', + postal: 'V8T 1E7', + province: 'BC', + country: 'Canada', + }, }; export const harley = { - id: "harley1", - userId: "uid-harley1", - name: "Harley Jones", - username: "harley@idir", - email: "harley@gov.ca", - roles: ["portal-user", "provider-user"], - isAdmin: false, - namespace: "aps-portal", - groups: null, - legalsAgreed: "[]", - sub: "sub", + id: 'harley1', + userId: 'uid-harley1', + name: 'Harley Jones', + username: 'harley@idir', + email: 'harley@gov.ca', + roles: ['portal-user', 'provider-user'], + isAdmin: false, + namespace: 'aps-portal', + groups: null, + legalsAgreed: '[]', + sub: 'sub', }; export const mark = { - id: "mark1", - userId: "uid-mark1", - name: "Mark Smith", - username: "markadmin", - email: "mark.smith@gov.ca", - roles: ["api-owner"], - isAdmin: true, - namespace: "aps-portal", - groups: null, - legalsAgreed: "[]", - sub: "sub", + id: 'mark1', + userId: 'uid-mark1', + name: 'Mark Smith', + username: 'markadmin', + email: 'mark.smith@gov.ca', + roles: ['api-owner'], + isAdmin: true, + namespace: 'aps-portal', + groups: null, + legalsAgreed: '[]', + sub: 'sub', }; diff --git a/src/nextapp/components/namespace-access/namespace-access-dialog.tsx b/src/nextapp/components/namespace-access/namespace-access-dialog.tsx index 05cdd5db4..46e8ad2bd 100644 --- a/src/nextapp/components/namespace-access/namespace-access-dialog.tsx +++ b/src/nextapp/components/namespace-access/namespace-access-dialog.tsx @@ -18,32 +18,66 @@ import { VStack, WrapItem, ButtonProps, + Text, } from '@chakra-ui/react'; import { FaPlusCircle } from 'react-icons/fa'; import startCase from 'lodash/startCase'; -import { UmaScope } from '@/shared/types/query.types'; +import { UmaPolicy, UmaScope } from '@/shared/types/query.types'; +import { AccessItem, Scope } from './types'; interface NamespaceAccessDialogProps { + accessItem?: AccessItem; buttonVariant?: ButtonProps['variant']; data: UmaScope[]; + onCancel?: () => void; onSubmit: (formData: FormData) => void; + serviceAccount?: UmaPolicy; variant: 'user' | 'service'; } const NamespaceAccessDialog: React.FC = ({ + accessItem, buttonVariant = 'ghost', data = [], + onCancel, onSubmit, + serviceAccount, variant = 'user', }) => { - const title = - variant === 'user' ? 'Grant user access' : 'Grant service account access'; + const isEditing = Boolean(accessItem) ?? Boolean(serviceAccount); + const title = React.useMemo(() => { + if (accessItem) { + return 'Edit access'; + } + if (variant === 'user') { + return 'Grant user access'; + } + return 'Grant service account access'; + }, [variant, accessItem]); const formRef = React.useRef(); + const getDefault = () => { + if (variant === 'user' && accessItem) { + return accessItem.scopes.map((s: Scope) => s.name); + } else if (variant === 'service' && serviceAccount) { + return serviceAccount?.scopes; + } + return []; + }; + const [selected, setSelected] = React.useState(getDefault); const { isOpen, onClose, onOpen } = useDisclosure(); + + // Events const handleGrantAccess = () => { const formData = new FormData(formRef?.current); + selected.forEach((scope) => { + formData.append('scopes', scope); + }); onSubmit(formData); - onClose(); + if (accessItem || serviceAccount) { + onCancel(); + } else { + onClose(); + } }; const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); @@ -52,19 +86,31 @@ const NamespaceAccessDialog: React.FC = ({ const handleSubmitClick = () => { formRef.current?.requestSubmit(); }; + const handleCancel = () => { + if (accessItem || serviceAccount) { + setSelected(getDefault()); + onCancel(); + } else { + setSelected([]); + onClose(); + } + }; return ( <> - + {buttonVariant && ( + + )} = ({ {variant === 'user' ? 'Email' : 'Service Account'} = ({ {data.map((r) => ( - - + + { + if (event.target.checked) { + setSelected((s) => [...s, r.name]); + } else { + setSelected((s) => + s.filter((n) => n !== r.name) + ); + } + }} + isChecked={selected.includes(r.name)} + > {r.name} + + {permissionHelpTextLookup[r.name] ?? ''} + ))} @@ -107,7 +171,7 @@ const NamespaceAccessDialog: React.FC = ({ @@ -126,4 +190,17 @@ const NamespaceAccessDialog: React.FC = ({ ); }; -export default NamespaceAccessDialog; +export default React.memo(NamespaceAccessDialog); + +const permissionHelpTextLookup = { + 'Access.Manage': + 'Can approve/reject access requests to your APIs that you make discoverable.', + 'Content.Publish': 'Can update the documentation on the portal.', + 'CredentialIssuer.Admin': + 'Can create Authorization Profiles so that they are available to be used when configuring Product Environments.', + 'GatewayConfig.Publish': + 'Can publish gateway configuration to Kong and to view the status of the upstreams.', + 'Namespace.Manage': + 'Can update the Access Control List for controlling access to viewing metrics, service configuration and service account management. This is a superuser for the Namespace.', + 'Namespace.View': 'Read-only access to the namespace.', +}; diff --git a/src/nextapp/components/namespace-access/service-accounts-access.tsx b/src/nextapp/components/namespace-access/service-accounts-access.tsx index f32c9edd0..6d8746967 100644 --- a/src/nextapp/components/namespace-access/service-accounts-access.tsx +++ b/src/nextapp/components/namespace-access/service-accounts-access.tsx @@ -21,6 +21,7 @@ import { useApi, useApiMutation } from '@/shared/services/api'; import { UmaPolicy, UmaScope } from '@/shared/types/query.types'; import { useQueryClient } from 'react-query'; import ActionsMenu from '../actions-menu'; +import { AccessItem } from './types'; interface ServiceAccountsAccessProps { resourceScopes: UmaScope[]; @@ -35,9 +36,11 @@ const ServiceAccountsAccess: React.FC = ({ }) => { const queryKey = 'namespaceAccessServiceAccounts'; const client = useQueryClient(); - const grant = useApiMutation(mutation); + const grant = useApiMutation(create); + const update = useApiMutation(mutation); const revoke = useApiMutation(revokeMutation); const toast = useToast(); + const [editing, setEditing] = React.useState(null); const [search, setSearch] = React.useState(''); const { data, isSuccess, isLoading } = useApi( queryKey, @@ -75,7 +78,7 @@ const ServiceAccountsAccess: React.FC = ({ return []; }, [data, isSuccess, search]); const handleGrantAccess = async (form: FormData) => { - const name = form.get('username') as string; + const name = form.get('email') as string; const scopes = form.getAll('scopes') as string[]; try { @@ -102,6 +105,37 @@ const ServiceAccountsAccess: React.FC = ({ }); } }; + const handleUpdateAccess = async (form: FormData) => { + const name = form.get('email') as string; + const scopes = form.getAll('scopes') as string[]; + + try { + await update.mutateAsync({ + prodEnvId, + resourceId, + data: { + name, + scopes, + }, + }); + toast({ + title: 'Access updated', + status: 'success', + isClosable: true, + }); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + isClosable: true, + status: 'error', + title: 'Unable to update access', + description: err, + }); + } + }; + const handleEditAccess = (d: UmaPolicy) => async () => { + setEditing(d); + }; const handleRevokeAccess = (policyId: string) => async () => { try { await revoke.mutateAsync({ @@ -124,15 +158,30 @@ const ServiceAccountsAccess: React.FC = ({ }); } }; + const handleSubmit = (form: FormData) => { + if (editing) { + handleUpdateAccess(form); + } else { + handleGrantAccess(form); + } + }; const accessRequestDialogProps = { data: resourceScopes, - onSubmit: handleGrantAccess, + onSubmit: handleSubmit, variant: 'service', } as const; return ( <> + {editing && ( + setEditing(null)} + /> + )} {requests?.length ?? '0'} service accounts @@ -200,6 +249,12 @@ const ServiceAccountsAccess: React.FC = ({ placement="bottom-end" data-testid={`nsa-sa-table-row-${index}-menu`} > + + Edit Access + = ({ prodEnvId, }) => { const queryKey = ['namespaceAccessUsers', resourceId]; + const [editing, setEditing] = React.useState(null); const [search, setSearch] = React.useState(''); const client = useQueryClient(); - const grant = useApiMutation(mutation); + const grant = useApiMutation(create); + const update = useApiMutation(mutation); const revoke = useApiMutation(revokeMutation); const toast = useToast(); const { data, isLoading, isSuccess } = useApi( @@ -77,8 +76,14 @@ const UsersAccess: React.FC = ({ ); const result = Object.keys(groupedByRequester).map((r) => { const requesterName = r.split('|')[1]; + const requesterEmail = get( + groupedByRequester[r], + '[0].requesterEmail', + requesterName + ); return { requesterName, + requesterEmail, scopes: groupedByRequester[r].map((d) => ({ id: d.scope, name: d.scopeName, @@ -122,6 +127,37 @@ const UsersAccess: React.FC = ({ }); } }; + const handleUpdateAccess = async (form: FormData) => { + const email = form.get('email') as string; + const scopes = form.getAll('scopes') as string[]; + + try { + await update.mutateAsync({ + prodEnvId, + data: { + resourceId, + email, + scopes, + }, + }); + toast({ + title: 'Access updated', + status: 'success', + isClosable: true, + }); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + status: 'error', + title: 'Unable to update user access', + description: err, + isClosable: true, + }); + } + }; + const handleEditAccess = (d: AccessItem) => async () => { + setEditing(d); + }; const handleRevokeAccess = (d: AccessItem) => async () => { try { await revoke.mutateAsync({ @@ -144,14 +180,30 @@ const UsersAccess: React.FC = ({ }); } }; + const handleSubmit = (formData: FormData) => { + if (editing) { + handleUpdateAccess(formData); + } else { + handleGrantAccess(formData); + } + }; + const accessRequestDialogProps = { data: resourceScopes, - onSubmit: handleGrantAccess, + onSubmit: handleSubmit, variant: 'user', } as const; return ( <> + {editing && ( + setEditing(null)} + /> + )} = ({ placement="bottom-end" data-testid={`nsa-users-table-row-${index}-menu`} > + + Edit Access + ; createServiceAccount?: Maybe; createUmaPolicy?: Maybe; + updateUmaPolicy?: Maybe; deleteUmaPolicy?: Maybe; grantPermissions?: Maybe>>; + updatePermissions?: Maybe>>; revokePermissions?: Maybe; approvePermissions?: Maybe; /** Authenticate and generate a token for a TemporaryIdentity with the Password Authentication Strategy. */ @@ -5340,6 +5342,13 @@ export type MutationCreateUmaPolicyArgs = { }; +export type MutationUpdateUmaPolicyArgs = { + prodEnvId: Scalars['ID']; + resourceId: Scalars['String']; + data: UmaPolicyInput; +}; + + export type MutationDeleteUmaPolicyArgs = { prodEnvId: Scalars['ID']; resourceId: Scalars['String']; @@ -5353,6 +5362,12 @@ export type MutationGrantPermissionsArgs = { }; +export type MutationUpdatePermissionsArgs = { + prodEnvId: Scalars['ID']; + data: UmaPermissionTicketInput; +}; + + export type MutationRevokePermissionsArgs = { prodEnvId: Scalars['ID']; resourceId: Scalars['String']; @@ -8345,6 +8360,7 @@ export type UmaPermissionTicket = { resourceName: Scalars['String']; requester: Scalars['String']; requesterName: Scalars['String']; + requesterEmail?: Maybe; owner: Scalars['String']; ownerName: Scalars['String']; granted: Scalars['Boolean']; diff --git a/src/server.ts b/src/server.ts index cad02ab59..f93239956 100644 --- a/src/server.ts +++ b/src/server.ts @@ -293,7 +293,10 @@ const configureExpress = (app: any) => { }); }); app.put('/feed/:entity', (req: any, res: any) => { - const context = keystone.createContext({ skipAccessControl: true }); + const context = keystone.createContext({ + skipAccessControl: true, + authentication: { item: { name: 'Feeder Bot' } }, + }); putFeedWorker(context, req, res).catch((err: any) => { console.log(err); res.status(400).json({ result: 'error', error: '' + err }); diff --git a/src/services/keycloak/permission-ticket-service.ts b/src/services/keycloak/permission-ticket-service.ts index af3e9af97..c8f300d41 100644 --- a/src/services/keycloak/permission-ticket-service.ts +++ b/src/services/keycloak/permission-ticket-service.ts @@ -34,6 +34,7 @@ export interface PermissionTicket { resourceName?: string; ownerName?: string; requesterName?: string; + requesterEmail?: string; // not part of keycloak API but injected after } export class KeycloakPermissionTicketService { diff --git a/src/services/keystone/types.ts b/src/services/keystone/types.ts index 336a1a272..50830dd08 100644 --- a/src/services/keystone/types.ts +++ b/src/services/keystone/types.ts @@ -4509,8 +4509,10 @@ export type Mutation = { forceDeleteNamespace?: Maybe; createServiceAccount?: Maybe; createUmaPolicy?: Maybe; + updateUmaPolicy?: Maybe; deleteUmaPolicy?: Maybe; grantPermissions?: Maybe>>; + updatePermissions?: Maybe>>; revokePermissions?: Maybe; approvePermissions?: Maybe; /** Authenticate and generate a token for a TemporaryIdentity with the Password Authentication Strategy. */ @@ -5340,6 +5342,13 @@ export type MutationCreateUmaPolicyArgs = { }; +export type MutationUpdateUmaPolicyArgs = { + prodEnvId: Scalars['ID']; + resourceId: Scalars['String']; + data: UmaPolicyInput; +}; + + export type MutationDeleteUmaPolicyArgs = { prodEnvId: Scalars['ID']; resourceId: Scalars['String']; @@ -5353,6 +5362,12 @@ export type MutationGrantPermissionsArgs = { }; +export type MutationUpdatePermissionsArgs = { + prodEnvId: Scalars['ID']; + data: UmaPermissionTicketInput; +}; + + export type MutationRevokePermissionsArgs = { prodEnvId: Scalars['ID']; resourceId: Scalars['String']; @@ -8345,6 +8360,7 @@ export type UmaPermissionTicket = { resourceName: Scalars['String']; requester: Scalars['String']; requesterName: Scalars['String']; + requesterEmail?: Maybe; owner: Scalars['String']; ownerName: Scalars['String']; granted: Scalars['Boolean']; diff --git a/src/services/uma2/policy-service.ts b/src/services/uma2/policy-service.ts index 411839118..2c9d7c165 100644 --- a/src/services/uma2/policy-service.ts +++ b/src/services/uma2/policy-service.ts @@ -94,6 +94,22 @@ export class UMAPolicyService { return result; } + public async updateUmaPolicy( + permissionId: string, + body: Policy + ): Promise { + const url = `${this.policyEndpoint}/${permissionId}`; + logger.debug('[updateUmaPolicy] %s', url); + const result = await fetch(url, { + method: 'put', + body: JSON.stringify(body), + headers: headers(this.accessToken) as any, + }) + .then(checkStatus) + .then((res) => res.text); + logger.debug('[updateUmaPolicy] RESULT %j', result); + } + public async deleteUmaPolicy(policyId: string) { const url = `${this.policyEndpoint}/${policyId}`; logger.debug('[deleteUmaPolicy] %s', url); diff --git a/src/services/workflow/namespace-activity.ts b/src/services/workflow/namespace-activity.ts index 164207d94..5a865f076 100644 --- a/src/services/workflow/namespace-activity.ts +++ b/src/services/workflow/namespace-activity.ts @@ -287,7 +287,7 @@ export class StructuredActivityService { public async logNamespaceAccess( success: boolean, - grantRevoke: 'granted' | 'revoked', + grantRevoke: 'granted' | 'updated' | 'revoked', entity: string, subjectType: 'user' | 'client', subject: string, diff --git a/src/services/workflow/ns-uma-perm-access.ts b/src/services/workflow/ns-uma-perm-access.ts new file mode 100644 index 000000000..c9be52a11 --- /dev/null +++ b/src/services/workflow/ns-uma-perm-access.ts @@ -0,0 +1,195 @@ +import { + EnvironmentContext, + getResourceSets, +} from '../../lists/extensions/Common'; +import { strict as assert } from 'assert'; +import { Logger } from '../../logger'; +import { + KeycloakPermissionTicketService, + KeycloakUserService, + PermissionTicket, +} from '../keycloak'; +import { StructuredActivityService } from './namespace-activity'; + +const logger = Logger('wf.nsumaperm'); + +export async function updatePermissions( + context: any, + envCtx: EnvironmentContext, + email: string, + scopes: string[], + resourceId: string, + grant: 'grant' | 'update' = 'update' +): Promise<{ userId: string; result: { id: string }[] }> { + logger.debug('[updatePermissions] %s : %s %s', email, resourceId, scopes); + + await enforceAccessToResource(envCtx, resourceId); + + const userApi = new KeycloakUserService(envCtx.openid.issuer); + await userApi.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + const user = await userApi.lookupUserByEmail(email, false, ['idir']); + const displayName = + userApi.getOneAttributeValue(user, 'display_name') || user.email; + + const result = []; + const permissionApi = new KeycloakPermissionTicketService( + envCtx.openid.issuer, + envCtx.accessToken + ); + + const currentPermissions = await getCurrentUserPermissions( + permissionApi, + resourceId, + user.id + ); + + assert.strictEqual( + grant === 'update' || currentPermissions.length === 0, + true, + 'User already granted permissions. Use edit access.' + ); + + const deletedScopes = await revokeUserPermissions( + permissionApi, + currentPermissions.filter((perm) => !scopes.includes(perm.scopeName)) + ); + + const addedScopes = scopes.filter( + (scope) => + currentPermissions.filter((perm) => perm.scopeName === scope).length == 0 + ); + + for (const scope of addedScopes) { + const permission = await permissionApi.createOrUpdatePermission( + resourceId, + user.id, + true, + scope + ); + result.push({ id: permission.id }); + } + + if (addedScopes.length + deletedScopes.length > 0) { + await new StructuredActivityService( + context.sudo(), + context.authedItem['namespace'] + ).logNamespaceAccess( + true, + grant == 'grant' ? 'granted' : 'updated', + 'namespace access', + 'user', + displayName, + [ + ...addedScopes.map((s) => `[+] ${s}`), + ...deletedScopes.map((s) => `[-] ${s}`), + ] + ); + } + + return { userId: user.id, result }; +} + +export async function revokePermissions( + context: any, + envCtx: EnvironmentContext, + resourceId: string, + ids: string[] +): Promise<{ userId: string }> { + logger.debug( + '[revokePermissions] permissions resource %s : perms %s', + resourceId, + ids + ); + + await enforceAccessToResource(envCtx, resourceId); + + const permissionApi = new KeycloakPermissionTicketService( + envCtx.openid.issuer, + envCtx.accessToken + ); + + const perms = await permissionApi.listPermissions({ + resourceId: resourceId, + returnNames: true, + }); + + const requesterIds = []; + const deletedScopes = []; + for (const permId of ids) { + const foundPerms = perms.filter((perm) => perm.id === permId); + assert.strictEqual(foundPerms.length, 1, 'Invalid Permission'); + deletedScopes.push(foundPerms[0].scopeName); + requesterIds.push(foundPerms[0].requester); + await permissionApi.deletePermission(permId); + } + + const userApi = new KeycloakUserService(envCtx.openid.issuer); + await userApi.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + const user = await userApi.lookupUserById(requesterIds.pop()); + const displayName = user.attributes.display_name || user.email; + + await new StructuredActivityService( + context.sudo(), + context.authedItem['namespace'] + ).logNamespaceAccess( + true, + 'revoked', + 'namespace access', + 'user', + displayName, + deletedScopes + ); + + return { userId: user.id }; +} + +export async function enforceAccessToResource( + envCtx: EnvironmentContext, + resourceId: string +) { + const resourceIds = await getResourceSets(envCtx); + assert.strictEqual( + resourceIds.filter((rid) => rid === resourceId).length, + 1, + 'Invalid Resource' + ); +} + +/** + * + * @param permissionApi The Keycloak Permission Ticket service + * @param resourceId The namespace resource that these permissions relate to + * @param userId The user that these permissions must belong to + * @param desiredScopes All scopes for this user and resource that you want + * @returns An array of deleted resource scopes + */ +async function revokeUserPermissions( + permissionApi: KeycloakPermissionTicketService, + perms: PermissionTicket[] +): Promise { + const deletedScopes = []; + // delete any scopes that are not in the desiredScopes list + for (const perm of perms) { + deletedScopes.push(perm.scopeName); + await permissionApi.deletePermission(perm.id); + } + return deletedScopes; +} + +async function getCurrentUserPermissions( + permissionApi: KeycloakPermissionTicketService, + resourceId: string, + userId: string +): Promise { + const perms = await permissionApi.listPermissions({ + resourceId, + returnNames: true, + }); + return perms.filter((perm) => perm.requester === userId); +} diff --git a/src/services/workflow/ns-uma-policy-access.ts b/src/services/workflow/ns-uma-policy-access.ts new file mode 100644 index 000000000..41a866355 --- /dev/null +++ b/src/services/workflow/ns-uma-policy-access.ts @@ -0,0 +1,122 @@ +import { EnvironmentContext } from '../../lists/extensions/Common'; +import { strict as assert } from 'assert'; +import { Logger } from '../../logger'; +import { StructuredActivityService } from './namespace-activity'; +import { enforceAccessToResource } from './ns-uma-perm-access'; +import { Policy, UMAPolicyService } from '../uma2'; + +const logger = Logger('wf.nsumaperm'); + +export async function createUmaPolicy( + context: any, + envCtx: EnvironmentContext, + resourceId: string, + policy: Policy +) { + logger.debug('[createUmaPolicy] %s %j', resourceId, policy); + + await enforceAccessToResource(envCtx, resourceId); + + const policyApi = new UMAPolicyService( + envCtx.uma2.policy_endpoint, + envCtx.accessToken + ); + + // name, scopes + const umaPolicy = await policyApi.createUmaPolicy(resourceId, policy); + + await new StructuredActivityService( + context.sudo(), + context.authedItem['namespace'] + ).logNamespaceAccess( + true, + 'granted', + 'namespace access', + 'client', + policy.name, + policy.scopes + ); + + return umaPolicy; +} + +export async function updateUmaPolicy( + context: any, + envCtx: EnvironmentContext, + resourceId: string, + clientId: string, + scopes: string[] +) { + logger.debug('[updateUmaPolicy] %s policy %j', resourceId, clientId); + + await enforceAccessToResource(envCtx, resourceId); + + const policyApi = new UMAPolicyService( + envCtx.uma2.policy_endpoint, + envCtx.accessToken + ); + + const policies = await policyApi.listPolicies({ resource: resourceId }); + const policy = policies + .filter((policy) => policy.clients?.includes(clientId)) + .pop(); + + assert.strictEqual(Boolean(policy), true, 'No policy found for client'); + + const addedScopes = scopes.filter((s) => !policy.scopes.includes(s)); + const deletedScopes = policy.scopes.filter((s) => !scopes.includes(s)); + + policy.scopes = scopes; + + await policyApi.updateUmaPolicy(policy.id, policy); + + await new StructuredActivityService( + context.sudo(), + context.authedItem['namespace'] + ).logNamespaceAccess( + true, + 'updated', + 'namespace access', + 'client', + policy.name, + [ + ...addedScopes.map((s) => `[+] ${s}`), + ...deletedScopes.map((s) => `[-] ${s}`), + ] + ); + + return policy; +} + +export async function revokeUmaPolicy( + context: any, + envCtx: EnvironmentContext, + resourceId: string, + policyId: string +) { + logger.debug('[revokeUmaPolicy] %s policy %s', resourceId, policyId); + + await enforceAccessToResource(envCtx, resourceId); + + const policyApi = new UMAPolicyService( + envCtx.uma2.policy_endpoint, + envCtx.accessToken + ); + + const policy = await policyApi.findPolicyByResource(resourceId, policyId); + logger.warn('Policy %j', policy); + + await policyApi.deleteUmaPolicy(policyId); + + await new StructuredActivityService( + context.sudo(), + context.authedItem['namespace'] + ).logNamespaceAccess( + true, + 'revoked', + 'namespace access', + 'client', + policy.name, + policy.scopes + ); +} diff --git a/src/test/integrated/uma2/permissionTicket.ts b/src/test/integrated/uma2/permissionTicket.ts new file mode 100644 index 000000000..b5a67b660 --- /dev/null +++ b/src/test/integrated/uma2/permissionTicket.ts @@ -0,0 +1,81 @@ +/* + +Wire up directly with Keycloak and use the Services + +To run: + + +npm run ts-build +export CID="" +export CSC="" +export ISSUER="" +npm run ts-watch +node dist/test/integrated/uma2/permissionTicket.js + +*/ + +import { + KeycloakPermissionTicketService, + KeycloakTokenService, + PermissionTicket, + PermissionTicketQuery, +} from '../../../services/keycloak'; +import { o } from '../util'; + +(async () => { + const tok = new KeycloakTokenService( + process.env.ISSUER + '/protocol/openid-connect/token' + ); + const token = await tok.getKeycloakSession(process.env.CID, process.env.CSC); + + const permissionApi = new KeycloakPermissionTicketService( + process.env.ISSUER, + token + ); + + const oldUser = 'acope@idir'; + const newUser = 'acope2@idir'; + + const oldUserId = '15a3cbbe-95b5-49f0-84ee-434a9b92d04a'; + const newUserId = '6a97baf4-0cef-4e57-a3c2-d2f84c41f07c'; + + const query: PermissionTicketQuery = { + requester: oldUserId, + returnNames: true, + }; + + const resPermsOld = await permissionApi.listPermissions({ + requester: oldUserId, + returnNames: true, + }); + const resPermsNew = await permissionApi.listPermissions({ + requester: newUserId, + returnNames: true, + }); + + const createPermission = async (perm: PermissionTicket) => { + await permissionApi.createPermission( + perm.resource, + newUserId, + true, + perm.scopeName + ); + }; + + function filterOutAlreadyExisting(p: PermissionTicket): boolean { + return ( + resPermsNew.filter( + (newp) => p.resource === newp.resource && p.scope === newp.scope + ).length == 0 + ); + } + const updates = await Promise.all( + resPermsOld.filter(filterOutAlreadyExisting).map(createPermission) + ); + console.log('Updates ' + updates.length); + + const deletePermission = async (perm: PermissionTicket) => { + await permissionApi.deletePermission(perm.id); + }; + //await Promise.all(resPermsNew.map(deletePermission)); +})(); From 9205fd5bad7902ed5ff389af7cb8ae3c908688a8 Mon Sep 17 00:00:00 2001 From: jTendeck <34200068+jTendeck@users.noreply.github.com> Date: Mon, 13 Feb 2023 12:53:03 -0800 Subject: [PATCH 5/8] Store GWA_RES_SVR_CLIENT_SECRET in Secret (#744) --- .github/workflows/ci-build-deploy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index cd5384866..151594af4 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -281,8 +281,10 @@ jobs: value: 'FB000000' GWA_RES_SVR_CLIENT_ID: value: '${{ secrets.OIDC_CLIENT_ID }}' + secure: true GWA_RES_SVR_CLIENT_SECRET: value: '${{ secrets.OIDC_CLIENT_SECRET }}' + secure: true KEYCLOAK_AUTH_URL: value: '${{ secrets.KEYCLOAK_AUTH }}' KEYCLOAK_REALM: From 78a93e2ff5b97373fb736b6758732b67464ffc62 Mon Sep 17 00:00:00 2001 From: ike thecoder Date: Mon, 13 Feb 2023 14:04:17 -0800 Subject: [PATCH 6/8] fix unit tests (#749) --- src/test/mocks/handlers/data/keycloak.yaml | 1 + src/test/services/org-groups/authz.test.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/mocks/handlers/data/keycloak.yaml b/src/test/mocks/handlers/data/keycloak.yaml index 972bf20dd..5a873fd26 100644 --- a/src/test/mocks/handlers/data/keycloak.yaml +++ b/src/test/mocks/handlers/data/keycloak.yaml @@ -38,6 +38,7 @@ groupDetails: attributes: org: ['ministry-of-citizens-services'] 'org-unit': ['databc'] + 'org-enabled': ['true'] groups: - id: a6902d51-38c1-43ca-8748-8d2c7209b1e1 diff --git a/src/test/services/org-groups/authz.test.ts b/src/test/services/org-groups/authz.test.ts index a6d1b4e43..fbab1645f 100644 --- a/src/test/services/org-groups/authz.test.ts +++ b/src/test/services/org-groups/authz.test.ts @@ -24,6 +24,7 @@ describe('Org Group Access Service', function () { const kc = new OrgAuthzService(uma2); const result = await kc.createIfMissingResource('newresource'); - expect(result).toBe('0001'); + expect(result.created).toBe(true); + expect(result.id).toBe('0001'); }); }); From 9145617164e2aca6f949ad7c34f3a680f60ac99f Mon Sep 17 00:00:00 2001 From: ike thecoder Date: Wed, 22 Feb 2023 00:22:42 -0800 Subject: [PATCH 7/8] fix missing metrics and add winston logger to feeder (#756) * fix missing metrics and add winston logger * get better info about transfers * use a stable stringify to calculate hash * fix duplicate consumer metrics in ops * fix consumer metrics and add route metrics * remove commented code --- feeds/index.js | 3 +- feeds/kong/index.js | 141 ++++---- feeds/logger.js | 36 ++ feeds/package-lock.json | 510 ++++++++++++++++++++++++++++- feeds/package.json | 3 +- feeds/prometheus/index.js | 47 ++- feeds/utils/transfers.js | 301 +++++++++-------- src/services/report/ops-metrics.ts | 71 +++- 8 files changed, 867 insertions(+), 245 deletions(-) create mode 100644 feeds/logger.js diff --git a/feeds/index.js b/feeds/index.js index bc416e130..33adc525c 100644 --- a/feeds/index.js +++ b/feeds/index.js @@ -1,3 +1,4 @@ +const { logger } = require('./logger'); const express = require('express'); const fetch = require('node-fetch'); const YAML = require('js-yaml'); @@ -173,7 +174,7 @@ if (process.env.SCHEDULE == 'true') { } const server = app.listen(port, () => { - console.log(`Listening at http://localhost:${port}`); + logger.info(`Listening at http://localhost:${port}`); }); process.on('SIGINT', () => process.kill(process.pid, 'SIGTERM')); diff --git a/feeds/kong/index.js b/feeds/kong/index.js index e389248ad..60827013d 100644 --- a/feeds/kong/index.js +++ b/feeds/kong/index.js @@ -1,12 +1,11 @@ const fs = require('fs'); const { transfers } = require('../utils/transfers'); const { portal } = require('../utils/portal'); - const { v4: uuidv4 } = require('uuid'); - -const assert = require('assert').strict; - const mask = require('./mask'); +const { Logger } = require('../logger'); + +const log = Logger('kong'); async function scopedSync( { url, workingPath, destinationUrl }, @@ -90,8 +89,8 @@ async function scopedSyncByNamespace( const nm = item.entity + ':' + cur.extForeignKey; await destination .fireAndForgetDeletion('/feed/' + item.entity + '/' + cur.extForeignKey) - .then((result) => console.log(`[${nm}] DELETED`)) - .catch((err) => console.log(`[${nm}] DELETION ERR ${err}`)); + .then((result) => log.debug(`[${nm}] DELETED`)) + .catch((err) => log.error(`[${nm}] DELETION ERR ${err}`)); } } @@ -181,9 +180,27 @@ function loadProducer(xfer, destinationUrl, file, name, type, feedPath) { const allACLs = type == 'consumer' ? xfer.get_json_content('gw-acls')['data'] : null; let index = 0; + + log.info('[producer] %s : %d records', file, items.length); + + const results = { + 'no-change': 0, + created: 0, + 'created-failed': 0, + updated: 0, + deleted: 0, + 'updated-failed': 0, + 'deleted-failed': 0, + }; + return () => { if (index == items.length) { - console.log('Finished producing ' + index + ' records.'); + Object.keys(results) + .filter((r) => results[r] != 0) + .forEach((r) => { + log.info('[%s] %d', String(r).padStart(15, ' '), results[r]); + }); + log.info('Finished producing ' + index + ' records.'); return null; } const item = items[index]; @@ -212,7 +229,7 @@ function loadProducer(xfer, destinationUrl, file, name, type, feedPath) { // if (item['plugins'].length == 0) { // return new Promise ((resolve, reject) => resolve()) // } - console.log(nm + ` with ${item['plugins'].length} plugins`); + // log.debug(nm + ` with ${item['plugins'].length} plugins`); if (type == 'consumer') { item['aclGroups'] = allACLs @@ -226,8 +243,14 @@ function loadProducer(xfer, destinationUrl, file, name, type, feedPath) { } return destination .fireAndForget(feedPath, item) - .then((result) => console.log(`[${nm}] OK`, result)) - .catch((err) => console.log(`[${nm}] ERR ${err}`)); + .then((result) => { + results[result['result']]++; + + log.debug('%s -> %s OK', file, result); + }) + .catch((err) => { + log.error(`[${nm}] ERR ${err}`); + }); }; } @@ -256,10 +279,28 @@ function loadGroupsProducer(xfer, destinationUrl, feedPath) { }); }); + log.info('[loadGroupsProducer] (%s) %d records', feedPath, items.length); + let index = 0; + + const results = { + 'no-change': 0, + created: 0, + 'created-failed': 0, + updated: 0, + deleted: 0, + 'updated-failed': 0, + 'deleted-failed': 0, + }; + return () => { if (index == items.length) { - console.log('Finished producing ' + index + ' records.'); + Object.keys(results) + .filter((r) => results[r] != 0) + .forEach((r) => { + log.info('[%s] %d', String(r).padStart(15, ' '), results[r]); + }); + log.info('Finished producing ' + index + ' records.'); return null; } const item = items[index]; @@ -274,79 +315,13 @@ function loadGroupsProducer(xfer, destinationUrl, feedPath) { return destination .fireAndForget(feedPath, item) - .then((result) => console.log(`[${nm}] OK`, result)) - .catch((err) => console.log(`[${nm}] ERR ${err}`)); - }; -} - -/* Initial Load of Service and Routes - Use a mapping file to map Service -> Product Name - -*/ -function loadServiceAccessProducer(xfer, destinationUrl, file, feedPath) { - const destination = portal(destinationUrl); - const items = []; - const allACLS = xfer.get_json_content('gw-acls')['data']; - - const allPlugins = xfer.get_json_content('gw-plugins')['data'].map(mask); - const nsGroups = []; - allPlugins - .filter((p) => p.name == 'acl') - .map((p) => { - return p.config.allow.map((a) => { - nsGroups.push({ - namespace: toNamespace(p.tags), - name: a, - service: p.service, - route: p.route, - }); + .then((result) => { + results[result['result']]++; + log.debug('%s -> %s OK', feedPath, result); + }) + .catch((err) => { + log.error(`[${nm}] ERR ${err}`); }); - }); - - xfer.get_json_content(file)['data'].map((item) => { - // Create an application of any Consumer that has atleast one ACL Group - const acls = allACLS - .filter( - (acl) => acl.group != 'idir' && acl.group != 'gwa_github_developer' - ) - .filter((acl) => acl.consumer.id == item.id); - - const consumerType = - item.username.endsWith('@sm-idir') || - item.username.endsWith('@idir') || - item.username.endsWith('@github') - ? 'user' - : 'client'; - - if (acls.length > 0) { - const acl_groups = acls.map((acl) => acl.group).join(', '); - // need to have it so that a ServiceAccess is created by each namespace that it relates to - // - items.push({ - id: 'mig-' + item.username + '-' + item.id, - active: true, - aclEnabled: true, - consumerType: consumerType, - consumer: item.username, - productEnvironment: '6053d7858bd8930018423480', - }); - } - }); - - let index = 0; - return () => { - if (index == items.length) { - console.log('Finished producing ' + index + ' records.'); - return null; - } - const item = items[index]; - index++; - const nm = item['id']; - - return destination - .fireAndForget(feedPath, item) - .then((result) => console.log(`[${nm}] OK`, result)) - .catch((err) => console.log(`[${nm}] ERR ${err}`)); }; } diff --git a/feeds/logger.js b/feeds/logger.js new file mode 100644 index 000000000..0adaa447f --- /dev/null +++ b/feeds/logger.js @@ -0,0 +1,36 @@ +const winston = require('winston'); + +const enumerateErrorFormat = winston.format((info) => { + if (info instanceof Error) { + Object.assign(info, { message: info.stack }); + } + return info; +}); + +const logger = Logger('general'); + +function Logger(category) { + return winston.createLogger({ + level: process.env.LOG_LEVEL || 'debug', + format: winston.format.combine( + enumerateErrorFormat(), + process.env.NODE_ENV === 'production' + ? winston.format.uncolorize() + : winston.format.colorize(), + winston.format.splat(), + winston.format.printf( + ({ level, message, stack }) => `${level}: [${category}] ${message}` + ) + ), + transports: [ + new winston.transports.Console({ + stderrLevels: ['error'], + }), + ], + }); +} + +module.exports = { + Logger, + logger, +}; diff --git a/feeds/package-lock.json b/feeds/package-lock.json index 193d2b87a..56447f586 100644 --- a/feeds/package-lock.json +++ b/feeds/package-lock.json @@ -13,9 +13,33 @@ "moment": "^2.29.1", "multer": "^1.4.2", "node-fetch": "^2.6.1", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "winston": "^3.8.2" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", + "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" + }, "node_modules/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -43,6 +67,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -88,6 +117,46 @@ "node": ">= 0.8" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", @@ -212,6 +281,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -281,6 +355,11 @@ "node": ">= 0.10.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -298,6 +377,11 @@ "node": ">= 0.8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -353,6 +437,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -369,6 +469,29 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/logform": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", + "dependencies": { + "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -502,6 +625,14 @@ "node": ">= 0.8" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -578,6 +709,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -630,6 +769,22 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -651,6 +806,11 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -659,6 +819,11 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -713,6 +878,120 @@ "node": ">= 0.8" } }, + "node_modules/winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "dependencies": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston-transport/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/winston-transport/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/winston/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -723,6 +1002,26 @@ } }, "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" + }, + "@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@types/triple-beam": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", + "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -747,6 +1046,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -783,6 +1087,46 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "requires": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", @@ -888,6 +1232,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -945,6 +1294,11 @@ "vary": "~1.1.2" } }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -959,6 +1313,11 @@ "unpipe": "~1.0.0" } }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -999,6 +1358,16 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -1012,6 +1381,31 @@ "argparse": "^2.0.1" } }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "logform": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", + "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", + "requires": { + "@colors/colors": "1.5.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1106,6 +1500,14 @@ "ee-first": "1.1.1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1167,6 +1569,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-stable-stringify": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1215,6 +1622,19 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -1230,11 +1650,21 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1274,6 +1704,84 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "winston": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", + "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "requires": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "winston-transport": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", + "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/feeds/package.json b/feeds/package.json index 84016eb55..99243f4aa 100644 --- a/feeds/package.json +++ b/feeds/package.json @@ -10,7 +10,8 @@ "moment": "^2.29.1", "multer": "^1.4.2", "node-fetch": "^2.6.1", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "winston": "^3.8.2" }, "scripts": { "start": "node index.js" diff --git a/feeds/prometheus/index.js b/feeds/prometheus/index.js index dde02fb53..741a94268 100644 --- a/feeds/prometheus/index.js +++ b/feeds/prometheus/index.js @@ -2,6 +2,9 @@ const querystring = require('querystring'); const moment = require('moment'); const { transfers } = require('../utils/transfers'); const { portal } = require('../utils/portal'); +const { Logger } = require('../logger'); + +const log = Logger('prometheus'); const queryRanges = [ { @@ -58,9 +61,7 @@ async function syncQueryRanges( { workingPath, url, destinationUrl }, params = { numDays: 5 } ) { - console.log( - 'Prometheus SYNC syncQueryRanges (DAYS=' + params.numDays + ') ' + url - ); + log.info('[syncQueryRanges] (DAYS=' + params.numDays + ') ' + url); const exceptions = []; xfer = transfers(workingPath, url, exceptions); @@ -80,7 +81,6 @@ async function syncQueryRanges( step: _query.step, _: moment().valueOf(), }; - console.log(day.fromNow()); const path = '/api/v1/query_range?' + querystring.stringify(params); await xfer.copy(path, 'query-' + _query.id + '-' + target); @@ -88,14 +88,16 @@ async function syncQueryRanges( } // Now, send to portal - await xfer.concurrentWork(producer(xfer, params.numDays, destinationUrl)); + await xfer.concurrentWork( + producer(xfer, 'query_range', params.numDays, destinationUrl) + ); } async function syncQueries( { workingPath, url, destinationUrl }, params = { numDays: 5 } ) { - console.log('Prometheus SYNC (DAYS=' + params.numDays + ') ' + url); + log.info('[syncQueries] (DAYS=' + params.numDays + ') ' + url); const exceptions = []; xfer = transfers(workingPath, url, exceptions); @@ -121,27 +123,38 @@ async function syncQueries( } // Now, send to portal - await xfer.concurrentWork(producer(xfer, params.numDays, destinationUrl)); + await xfer.concurrentWork( + producer(xfer, 'query', params.numDays, destinationUrl) + ); } -function producer(xfer, numDays, destinationUrl) { +function producer(xfer, queryType, numDays, destinationUrl) { const destination = portal(destinationUrl); const work = []; for (var d = 0; d < numDays; d++) { const target = moment().add(-d, 'days').format('YYYY-MM-DD'); - for (_query of queries) { - xfer - .get_json_content('query-' + _query.id + '-' + target) - ['data'][0]['result'].map((metric) => { - work.push({ target: target, query: _query, metric: metric }); - }); + for (_query of queryType === 'query' ? queries : queryRanges) { + const json = xfer.get_json_content('query-' + _query.id + '-' + target); + const results = Array.isArray(json['data']) + ? json['data'][0]['result'] + : json['data']['result']; + log.info( + '[producer] %s %s : %d records', + target, + _query.id, + results.length + ); + results.map((metric) => { + work.push({ target: target, query: _query, metric: metric }); + }); } } + let index = 0; return () => { if (index == work.length) { - console.log('Finished producing ' + index + ' records.'); + log.info('Finished producing ' + index + ' records.'); return null; } const item = work[index]; @@ -167,8 +180,8 @@ function producer(xfer, numDays, destinationUrl) { return destination .fireAndForget('/feed/Metric', item.metric) - .then((result) => console.log(`[${name}] OK`, result)) - .catch((err) => console.log(`[${name}] ERR ${err}`)); + .then((result) => log.debug(`[${name}] OK`, result)) + .catch((err) => log.error(`[${name}] ERR ${err}`)); }; } diff --git a/feeds/utils/transfers.js b/feeds/utils/transfers.js index b32f29fb4..fdd466ab4 100644 --- a/feeds/utils/transfers.js +++ b/feeds/utils/transfers.js @@ -1,151 +1,180 @@ -const fs = require('fs') -const fetch = require('node-fetch') +const fs = require('fs'); +const fetch = require('node-fetch'); const url = require('url'); -const PromisePool = require('es6-promise-pool') -const { checkStatus } = require('./checkStatus') - -function transfers (workingPath, baseUrl, exceptions) { - fs.mkdirSync(workingPath, { recursive: true }) - - return { - copy: async function (_url, filename, index = 0) { - console.log("Fetching " + baseUrl + _url) - const out = workingPath + '/' + filename + '-' + index + '.json' - return fetch (baseUrl + _url) - .then (checkStatus) - .then (data => data.json()) - .then (json => { - fs.writeFileSync(out, JSON.stringify(json, null, 4), null); - console.log("WROTE "+ filename) - if (json.next != null) { - this.copy (json.next, filename, index + 1 ) - } else if ('result' in json && json['result'].length > 0) { - const u = url.parse(baseUrl + _url,true) - if ('limit' in u.query) { - const newUrl = `${u.pathname}?limit=${u.query.limit}&offset=${Number(u.query.offset) + Number(u.query.limit)}` - this.copy (newUrl, filename, index + 1) - } - } - }) - .catch (err => { - console.log("COPY ERROR " + filename + " - " + err) - exceptions.push({relativeUrl:url, filename:filename, error:"" + err}) - }) - }, +const PromisePool = require('es6-promise-pool'); +const { checkStatus } = require('./checkStatus'); +const { Logger } = require('../logger'); +const stringify = require('json-stable-stringify'); - copyOne: async function (_url, filename, index = 0) { - console.log("Fetching " + baseUrl + _url) - const out = workingPath + '/' + filename + '-' + index + '.json' - return fetch (baseUrl + _url) - .then (checkStatus) - .then (data => data.json()) - .then (json => { - fs.writeFileSync(out, JSON.stringify({data: [ json ] }, null, 4), null); - console.log("WROTE "+ filename) - }) - .catch (err => { - console.log("COPYONE ERROR " + filename + " - " + err) - exceptions.push({relativeUrl:url, filename:filename, error:"" + err}) - }) - }, +const log = Logger('utils.xfer'); - read: function (filename) { - const infile = workingPath + '/' + filename + '.json' - return JSON.parse(fs.readFileSync(infile)) - }, +function transfers(workingPath, baseUrl, exceptions) { + fs.mkdirSync(workingPath, { recursive: true }); - concurrentWork: async function(producer, concurrency = 5) { - var pool = new PromisePool(await producer, concurrency) - - // Start the pool. - var poolPromise = pool.start() - - // Wait for the pool to settle. - return poolPromise.then(function () { - console.log('All promises fulfilled') - }, function (error) { - console.log('Some promise rejected: ' + error.message) - throw Error ('Some promise rejected: ' + error.message) - }) - - }, + return { + copy: async function (_url, filename, index = 0) { + log.debug('[copy] %s%s', baseUrl, _url); + const out = workingPath + '/' + filename + '-' + index + '.json'; + return fetch(baseUrl + _url) + .then(checkStatus) + .then((data) => data.json()) + .then((json) => { + fs.writeFileSync(out, JSON.stringify(json, null, 4), null); + if (json.next != null) { + this.copy(json.next, filename, index + 1); + } else if ('result' in json && json['result'].length > 0) { + const u = url.parse(baseUrl + _url, true); + if ('limit' in u.query) { + const newUrl = `${u.pathname}?limit=${u.query.limit}&offset=${ + Number(u.query.offset) + Number(u.query.limit) + }`; + this.copy(newUrl, filename, index + 1); + } + } + }) + .catch((err) => { + log.error('[copy] %s, %s', filename, err); + exceptions.push({ + relativeUrl: url, + filename: filename, + error: '' + err, + }); + }); + }, - iterate_through_json_content_sync: function iterate_through_json_content_sync(location, next) { - const files = fs.readdirSync(workingPath + "/" + location) - files.forEach((file) => { - data = JSON.parse(fs.readFileSync(workingPath + "/" + location + '/' + file)) - next(file, data) - }) - }, - - get_file_list: function iterate_through_json_content(location) { - return fs.readdirSync(workingPath + "/" + location) - }, + copyOne: async function (_url, filename, index = 0) { + log.debug('[copyOne] %s%s', baseUrl, _url); + const out = workingPath + '/' + filename + '-' + index + '.json'; + return fetch(baseUrl + _url) + .then(checkStatus) + .then((data) => data.json()) + .then((json) => { + fs.writeFileSync( + out, + JSON.stringify({ data: [json] }, null, 4), + null + ); + }) + .catch((err) => { + log.error('[copyOne] %s, %s', filename, err); + exceptions.push({ + relativeUrl: url, + filename: filename, + error: '' + err, + }); + }); + }, - iterate_through_json_content: function iterate_through_json_content(location, next) { - fs.readdir(workingPath + "/" + location, (err, files) => { - if (err) { - throw(err) - } - files.forEach((file) => { - data = JSON.parse(fs.readFileSync(workingPath + "/" + location + '/' + file)) - next(file, data) - }) - }) - }, - - get_json_content: function get_json_content(file) { - let index = 0 - let data = [] - while (true) { - filePath = workingPath + '/' + file + "-" + index + ".json" - console.log("READ " + filePath) - if (fs.existsSync(filePath)) { - fileData = JSON.parse(fs.readFileSync(filePath)) - data = data.concat(fileData['data']) - index++ - } else { - console.log("RETURNING " + data.length) - return { next: null, data: data } - } - } - }, + read: function (filename) { + const infile = workingPath + '/' + filename + '.json'; + return JSON.parse(fs.readFileSync(infile)); + }, - get_list_ids: function get_list_ids(file) { - let index = 0 - let data = [] - while (true) { - filePath = workingPath + '/' + file + "-" + index + ".json" - console.log("READ " + filePath) - if (fs.existsSync(filePath)) { - fileData = JSON.parse(fs.readFileSync(filePath)) - data = data.concat(fileData['result']) - index++ - } else { - console.log("RETURNING " + data.length) - return { next: null, data: data } - } - } - }, + concurrentWork: async function (producer, concurrency = 5) { + var pool = new PromisePool(await producer, concurrency); - create_key_map: function create_key_map (list, idKey) { - const map = {} - for (item of list) { - map[item[idKey]] = item - } - return map + // Start the pool. + var poolPromise = pool.start(); + + // Wait for the pool to settle. + return poolPromise.then( + function () { + log.info('All promises fulfilled'); }, + function (error) { + log.error('Some promise rejected: ' + error.message); + throw Error('Some promise rejected: ' + error.message); + } + ); + }, - inject_hash_and_source: function (source, payload) { - const crypto = require('crypto') - const body = JSON.stringify(payload) + iterate_through_json_content_sync: + function iterate_through_json_content_sync(location, next) { + const files = fs.readdirSync(workingPath + '/' + location); + files.forEach((file) => { + data = JSON.parse( + fs.readFileSync(workingPath + '/' + location + '/' + file) + ); + next(file, data); + }); + }, + + get_file_list: function iterate_through_json_content(location) { + return fs.readdirSync(workingPath + '/' + location); + }, + + iterate_through_json_content: function iterate_through_json_content( + location, + next + ) { + fs.readdir(workingPath + '/' + location, (err, files) => { + if (err) { + throw err; + } + files.forEach((file) => { + data = JSON.parse( + fs.readFileSync(workingPath + '/' + location + '/' + file) + ); + next(file, data); + }); + }); + }, - payload['extSource'] = source - payload['extRecordHash'] = crypto.createHash('sha256').update(body).digest('hex') + get_json_content: function get_json_content(file) { + let index = 0; + let data = []; + while (true) { + filePath = workingPath + '/' + file + '-' + index + '.json'; + log.debug('[get_json_content] ' + filePath); + if (fs.existsSync(filePath)) { + fileData = JSON.parse(fs.readFileSync(filePath)); + data = data.concat(fileData['data']); + index++; + } else { + log.debug('[get_json_content] %s records = %d', file, data.length); + return { next: null, data: data }; } - } + } + }, + + get_list_ids: function get_list_ids(file) { + let index = 0; + let data = []; + while (true) { + filePath = workingPath + '/' + file + '-' + index + '.json'; + log.debug('[get_list_ids] ' + filePath); + if (fs.existsSync(filePath)) { + fileData = JSON.parse(fs.readFileSync(filePath)); + data = data.concat(fileData['result']); + index++; + } else { + log.info('[get_list_ids] records = ' + data.length); + return { next: null, data: data }; + } + } + }, + + create_key_map: function create_key_map(list, idKey) { + const map = {}; + for (item of list) { + map[item[idKey]] = item; + } + return map; + }, + + inject_hash_and_source: function (source, payload) { + const crypto = require('crypto'); + const body = stringify(payload); + + payload['extSource'] = source; + payload['extRecordHash'] = crypto + .createHash('sha256') + .update(body) + .digest('hex'); + }, + }; } module.exports = { - transfers: transfers -} \ No newline at end of file + transfers: transfers, +}; diff --git a/src/services/report/ops-metrics.ts b/src/services/report/ops-metrics.ts index 772c3a319..db13af229 100644 --- a/src/services/report/ops-metrics.ts +++ b/src/services/report/ops-metrics.ts @@ -5,6 +5,7 @@ import { Activity, Environment, GatewayConsumer, + GatewayRoute, Namespace, ServiceAccess, TemporaryIdentity, @@ -51,6 +52,7 @@ export class OpsMetrics { gActivity: Gauge; gConsumers: Gauge; gProducts: Gauge; + gGatewayRoutes: Gauge; constructor(keystone: Keystone) { this.keystone = keystone; @@ -107,6 +109,7 @@ export class OpsMetrics { ], }); + // value = 'requests_30_day' this.gConsumers = new Gauge({ name: 'ops_metrics_consumers', help: 'Consumer Access', @@ -121,10 +124,15 @@ export class OpsMetrics { 'environment', 'flow', 'issuer', - 'requests_30_day', 'date', ], }); + + this.gGatewayRoutes = new Gauge({ + name: 'ops_metrics_routes', + help: 'Gateway Service Route information', + labelNames: ['namespace', 'service', 'route', 'host_path', 'methods'], + }); } // public getRegister() { @@ -138,6 +146,7 @@ export class OpsMetrics { await this.generateActivityMetrics(); await this.generateConsumerMetrics(); await this.generateProductMetrics(); + await this.generateRouteMetrics(); } public async store() { @@ -154,6 +163,7 @@ export class OpsMetrics { this.gActivity, this.gProducts, this.gConsumers, + this.gGatewayRoutes, ]) { const existing = await batch.lookup('allBlobs', 'ref', metric.name(), []); if (existing) { @@ -367,13 +377,12 @@ export class OpsMetrics { product: sa.productEnvironment?.product?.name, environment: sa.productEnvironment?.name, flow: sa.productEnvironment?.flow, - requests_30_day: calcMetrics( - sa.productEnvironment?.product?.namespace, - sa.consumer?.username - ).totalRequests, date: sa.createdAt, }, - 1 + calcMetrics( + sa.productEnvironment?.product?.namespace, + sa.consumer?.username + ).totalRequests ); }); } @@ -413,6 +422,42 @@ export class OpsMetrics { ); }); } + + /* + name: 'ops_metrics_routes', + help: 'Gateway Service Route information', + labelNames: ['namespace', 'service', 'route', 'host_path', 'methods'], + */ + async generateRouteMetrics() { + const ctx = this.keystone.createContext({ + skipAccessControl: true, + authentication: { item: {} }, + }); + const routes = await getAllRoutes(ctx); + routes.forEach((route: GatewayRoute) => { + const hosts = JSON.parse(route.hosts); + const paths = route.paths ? JSON.parse(route.paths) : []; + const methods = + route.methods && route.methods != '[]' + ? JSON.parse(route.methods) + : ['*']; + + for (const host of hosts) { + for (const path of paths) { + this.gGatewayRoutes.set( + { + namespace: route.namespace, + route: route.name, + service: route.service?.name, + host_path: `${host}${path}`, + methods: JSON.stringify(methods), + }, + 1 + ); + } + } + }); + } } export async function getNamespaces(context: any): Promise { @@ -460,6 +505,20 @@ async function getAllEnvironments(ctx: any) { return allEnvs; } +async function getAllRoutes(ctx: any) { + const batch = new BatchService(ctx); + + // Limiting to 1000 is not great! We should really recurse until we get to the end! + const allEnvs = await batch.listAll( + 'allGatewayRoutes', + ['name', 'namespace', 'hosts', 'paths', 'methods', 'service { name }'], + undefined, + 0, + 1000 + ); + return allEnvs; +} + async function getAllConsumers(ctx: any) { const batch = new BatchService(ctx); From a82a34d56868e28d0f31a27878612ce723294298 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Wed, 22 Feb 2023 10:47:35 -0800 Subject: [PATCH 8/8] Add delete application confirmation, new namespace validation (#753) * Add delete application confirmation, new namespace validation * Update ci-build-deploy.yaml * fix for validating environment * fix grant permissions gql whitelist * Content fixes * Fix issuer select * Fix formatting of delete application dialog text --------- Co-authored-by: ike thecoder --- .github/workflows/ci-build-deploy.yaml | 6 +- src/authz/matrix.csv | 1 + src/mocks/handlers.js | 16 +++++ .../credential-issuer-select.tsx | 6 +- .../environment-edit/authorization-flow.tsx | 4 +- .../new-namespace/new-namespace.tsx | 40 ++++++++---- .../pages/devportal/applications/index.tsx | 62 +++++++++++++++++-- src/services/keystone/gateway-service.ts | 17 ++++- src/services/keystone/product-environment.ts | 12 ++-- 9 files changed, 129 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index 151594af4..d8d290c90 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -122,10 +122,12 @@ jobs: maxUnavailable: 100% readinessProbe: - timeoutSeconds: 20 + timeoutSeconds: 30 + periodSeconds: 120 livenessProbe: - timeoutSeconds: 20 + timeoutSeconds: 30 + periodSeconds: 120 persistence: enabled: true diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index 80c827723..619dd3e2d 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -128,6 +128,7 @@ API Owner Role Rules,,namespace,,,,,,,api-owner,,,allow, API Owner Role Rules,,currentNamespace,,,,,,,,,"portal-user,api-owner,provider-user,access-manager,credential-admin",allow, API Owner / Provider Role Rules,,updateCurrentNamespace,,,,,,,api-owner,,,allow, API Owner Role Rules,,updatePermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index 2e5256b3e..e15196bd5 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -196,6 +196,18 @@ export const handlers = [ }), keystone.mutation('CreateNamespace', (req, res, ctx) => { const { name } = req.variables; + + if (allNamespaces.map((n) => n.name).includes(name)) { + return res( + ctx.data({ + errors: [ + { + message: 'Namespace already exists', + }, + ], + }) + ); + } const id = `ns-${allNamespaces.length + 1}`; const namespace = { name, @@ -228,6 +240,10 @@ export const handlers = [ keystone.mutation('DeleteEnvironment', deleteEnvironmentHandler), keystone.query('GetOwnedEnvironment', getEnvironmentHandler), keystone.query('GetAllCredentialIssuers', getAllCredentialIssuersByNamespace), + keystone.query( + 'GetAllCredentialIssuersByNamespace', + getAllCredentialIssuersByNamespace + ), keystone.query('GetCredentialIssuers', getAllCredentialIssuers), keystone.query('SharedIdPPreview', getSharedIdpPreview), keystone.mutation('CreateAuthzProfile', createAuthzProfile), diff --git a/src/nextapp/components/environment-config/credential-issuer-select.tsx b/src/nextapp/components/environment-config/credential-issuer-select.tsx index 3a04cc92d..30e788880 100644 --- a/src/nextapp/components/environment-config/credential-issuer-select.tsx +++ b/src/nextapp/components/environment-config/credential-issuer-select.tsx @@ -14,7 +14,7 @@ const CredentialIssuerSelect: React.FC = ({ onChange, value, }) => { - const isEnabled = flow !== 'client-credentials'; + const isEnabled = flow === 'client-credentials'; const { data, isLoading, isError } = useApi( ['environment-credential-users', flow], { @@ -31,12 +31,12 @@ const CredentialIssuerSelect: React.FC = ({ + {error && {error}} + + Names must be: + + + Alphanumeric (letters and numbers only, no special + characters) + + Unique to all other namespaces + + @@ -104,7 +120,7 @@ const NewNamespace: React.FC = ({ isOpen, onClose }) => { onClick={handleCreateNamespace} data-testid="ns-modal-create-btn" > - Create + Create Namespace diff --git a/src/nextapp/pages/devportal/applications/index.tsx b/src/nextapp/pages/devportal/applications/index.tsx index 238e920e4..5361ea700 100644 --- a/src/nextapp/pages/devportal/applications/index.tsx +++ b/src/nextapp/pages/devportal/applications/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Alert, AlertIcon, + Button, Container, Text, Heading, @@ -14,6 +15,13 @@ import { Grid, GridItem, Flex, + AlertDialog, + AlertDialogOverlay, + AlertDialogContent, + AlertDialogFooter, + AlertDialogBody, + AlertDialogCloseButton, + AlertDialogHeader, } from '@chakra-ui/react'; import EditApplication from '@/components/edit-application'; import get from 'lodash/get'; @@ -57,8 +65,12 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const ApplicationsPage: React.FC< InferGetServerSidePropsType -> = ({ queryKey }) => { +> = ({ queryKey }): JSX.Element => { const toast = useToast(); + const cancelRef = React.useRef(); + const [appToDelete, setAppToDelete] = React.useState( + null + ); const [editing, setEditing] = React.useState(null); const [openId, setOpenId] = React.useState(); const queryClient = useQueryClient(); @@ -101,12 +113,19 @@ const ApplicationsPage: React.FC< const handleCloseEditDialog = () => { setEditing(null); }; - const handleDelete = (id: string) => async () => { + const handleSelectToDelete = (app: Application) => () => { + setAppToDelete(app); + }; + const handleClose = () => { + setAppToDelete(null); + }; + const handleDelete = async () => { + setAppToDelete(null); try { - if (openId === id) { + if (openId === appToDelete.id) { setOpenId(null); } - await deleteMutation.mutateAsync({ id }); + await deleteMutation.mutateAsync({ id: appToDelete.id }); toast({ title: 'Application deleted', status: 'success', @@ -147,6 +166,37 @@ const ApplicationsPage: React.FC< onClose={handleCloseEditDialog} refreshQueryKey={queryKey} /> + + + + Delete Application + + + You are about to delete {appToDelete?.name}. Deleting an application + will delete all related credentials found under "My Access". This + action cannot be undone. + + + + + + + {data?.allApplications?.length === 0 && ( @@ -189,9 +239,9 @@ const ApplicationsPage: React.FC< - Delete Application + Delete Application... { + svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))); + + svc.routes?.map((route) => { + route.plugins?.map( + (plugin) => (plugin.config = JSON.parse(plugin.config)) + ); + }); + }); +} + export async function lookupServices( context: any, serviceIds: string[] @@ -30,9 +42,8 @@ export async function lookupServices( variables: { services: serviceIds }, }); logger.debug('Query result %j', result); - result.data.allGatewayServices.map((svc: GatewayService) => - svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))) - ); + parsePluginConfig(result.data.allGatewayServices); + return result.data.allGatewayServices; } diff --git a/src/services/keystone/product-environment.ts b/src/services/keystone/product-environment.ts index dbb73a6ef..e710d0744 100644 --- a/src/services/keystone/product-environment.ts +++ b/src/services/keystone/product-environment.ts @@ -1,4 +1,5 @@ import { Logger } from '../../logger'; +import { parsePluginConfig } from './gateway-service'; import { Environment, EnvironmentWhereInput, @@ -67,9 +68,8 @@ export async function lookupProductEnvironmentServices( 'ProductEnvironmentNotFound ' + prodEnvId ); - result.data.allEnvironments[0].services.map((svc: GatewayService) => - svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))) - ); + parsePluginConfig(result.data.allEnvironments[0].services); + return result.data.allEnvironments[0]; } @@ -117,9 +117,9 @@ export async function lookupProductEnvironmentServicesBySlug( 1, 'ProductEnvironmentNotFound By Slug ' + appId ); - result.data.allEnvironments[0].services.map((svc: GatewayService) => - svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))) - ); + + parsePluginConfig(result.data.allEnvironments[0].services); + return result.data.allEnvironments[0]; }