From 78f94295964c4d129ea8191071d3dfb52839d095 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Wed, 5 Jun 2024 13:36:38 -0700 Subject: [PATCH] add cypress tests for v3 of api --- e2e/cypress.config.ts | 5 +- e2e/cypress/fixtures/api-v3.json | 6 + e2e/cypress/support/auth-commands.ts | 651 +++++++------- e2e/cypress/support/global.d.ts | 108 ++- ...10-jwt-genkp-access-approve-api-rqst.cy.ts | 5 +- ...t-publlicKey-access-approve-api-rqst.cy.ts | 81 +- e2e/cypress/tests/19-api-v3/api-suite.ts | 795 ++++++++++++++++++ e2e/package-lock.json | 266 +++--- e2e/package.json | 1 + local/keycloak/master-realm.json | 6 +- src/auth/auth-tsoa.ts | 7 +- src/batch/data-rules.js | 2 +- src/batch/feed-worker.ts | 11 + src/controllers/ioc/keystoneInjector.ts | 2 +- src/controllers/v3/GatewayController.ts | 13 +- src/controllers/v3/IdentifierController.ts | 5 +- src/controllers/v3/OrganizationController.ts | 38 +- src/controllers/v3/ProductController.ts | 4 +- src/controllers/v3/openapi.yaml | 94 ++- src/controllers/v3/routes.ts | 76 +- src/services/identifiers.ts | 2 + src/services/keycloak/group-service.ts | 18 + src/services/org-groups/namespace.ts | 4 +- src/test/services/batch/batch-utils.test.js | 31 + src/tsoa-v3.json | 4 - 25 files changed, 1708 insertions(+), 527 deletions(-) create mode 100644 e2e/cypress/fixtures/api-v3.json create mode 100644 e2e/cypress/tests/19-api-v3/api-suite.ts diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts index 2cf4bc2a3..1041093a2 100644 --- a/e2e/cypress.config.ts +++ b/e2e/cypress.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ './cypress/tests/16-*/*.ts', './cypress/tests/17-*/*.ts', './cypress/tests/18-*/*.ts', + './cypress/tests/19-*/*.ts', ] return config }, @@ -59,7 +60,7 @@ export default defineConfig({ env: { CLIENT_ID: 'aps-portal', CLIENT_SECRET: '8e1a17ed-cb93-4806-ac32-e303d1c86018', - OIDC_ISSUER: 'http://keycloak.localtest.me:9081', + OIDC_ISSUER: 'http://keycloak.localtest.me:9081/auth/realms/master', TOKEN_URL: 'http://keycloak.localtest.me:9081/auth/realms/master/protocol/openid-connect/token', GWA_API_URL: 'http://gwa-api.localtest.me:2000/v2', @@ -69,6 +70,8 @@ export default defineConfig({ BASE_URL: 'http://oauth2proxy.localtest.me:4180', KEYCLOAK_URL: 'http://keycloak.localtest.me:9081', WEBAPP_URL: 'http://html-sample-app.localtest.me:4242', + DEV_USERNAME: 'janis@idir', + DEV_PASSWORD: 'awsummer', }, retries: { runMode: 2, diff --git a/e2e/cypress/fixtures/api-v3.json b/e2e/cypress/fixtures/api-v3.json new file mode 100644 index 000000000..9ecb8f180 --- /dev/null +++ b/e2e/cypress/fixtures/api-v3.json @@ -0,0 +1,6 @@ +{ + "gateway": { + "displayName": "My Gateway" + }, + "model": [] +} diff --git a/e2e/cypress/support/auth-commands.ts b/e2e/cypress/support/auth-commands.ts index fd289af65..5256a563f 100644 --- a/e2e/cypress/support/auth-commands.ts +++ b/e2e/cypress/support/auth-commands.ts @@ -7,15 +7,15 @@ import { checkElementExists } from './e2e' // import _ = require('cypress/types/lodash') const njwt = require('njwt') -const fs = require('fs'); +const fs = require('fs') const config = require('../fixtures/manage-control/kong-plugin-config.json') const jose = require('node-jose') -const YAML = require('yamljs'); +const YAML = require('yamljs') -const forge = require('node-forge'); +const forge = require('node-forge') let headers: any @@ -81,10 +81,16 @@ Cypress.Commands.add('keycloakLogin', (username: string, password: string) => { Cypress.Commands.add('getLastConsumerID', () => { let id: any - cy.get('[data-testid="all-consumer-control-tbl"]').find('tr').last().find('td').first().find('a').then(($text) => { - id = $text.text() - return id - }) + cy.get('[data-testid="all-consumer-control-tbl"]') + .find('tr') + .last() + .find('td') + .first() + .find('a') + .then(($text) => { + id = $text.text() + return id + }) }) Cypress.Commands.add('resetCredential', (accessRole: string) => { @@ -111,31 +117,34 @@ Cypress.Commands.add('resetCredential', (accessRole: string) => { }) }) -Cypress.Commands.add('getUserSessionTokenValue', (namespace: string, isNamespaceSelected?: true) => { - const login = new LoginPage() - const home = new HomePage() - const na = new NamespaceAccessPage() - let userSession: string - cy.visit('/') - cy.reload() - cy.fixture('apiowner').as('apiowner') - cy.preserveCookies() - cy.visit(login.path) - cy.getUserSession().then(() => { - cy.get('@apiowner').then(({ user }: any) => { - cy.login(user.credentials.username, user.credentials.password) - cy.log('Logged in!') - // home.useNamespace(apiTest.namespace) - if (isNamespaceSelected || undefined) { - home.useNamespace(namespace) - } - cy.get('@login').then(function (xhr: any) { - userSession = xhr.response.headers['x-auth-request-access-token'] - return userSession +Cypress.Commands.add( + 'getUserSessionTokenValue', + (namespace: string, isNamespaceSelected?: true) => { + const login = new LoginPage() + const home = new HomePage() + const na = new NamespaceAccessPage() + let userSession: string + cy.visit('/') + cy.reload() + cy.fixture('apiowner').as('apiowner') + cy.preserveCookies() + cy.visit(login.path) + cy.getUserSession().then(() => { + cy.get('@apiowner').then(({ user }: any) => { + cy.login(user.credentials.username, user.credentials.password) + cy.log('Logged in!') + // home.useNamespace(apiTest.namespace) + if (isNamespaceSelected || undefined) { + home.useNamespace(namespace) + } + cy.get('@login').then(function (xhr: any) { + userSession = xhr.response.headers['x-auth-request-access-token'] + return userSession + }) }) }) - }) -}) + } +) Cypress.Commands.add('getUserSessionResponse', () => { cy.getUserSession().then(() => { @@ -188,14 +197,13 @@ Cypress.Commands.add('loginByAuthAPI', (username: string, password: string) => { ...user, }, } - cy.log(JSON.stringify(userItem)) + cy.wrap(userItem).as('loginByAuthApiResponse') }) log.snapshot('after') log.end() }) Cypress.Commands.add('logout', () => { - cy.log('< Logging out') cy.getSession().then(() => { cy.get('@session').then((res: any) => { @@ -209,7 +217,6 @@ Cypress.Commands.add('logout', () => { }) Cypress.Commands.add('keycloakLogout', () => { - cy.log('< Logging out') cy.get('.dropdown-toggle.ng-binding').click() cy.contains('Sign Out').click() @@ -228,7 +235,7 @@ Cypress.Commands.add('getAccessToken', (client_id: string, client_secret: string client_secret, }, form: true, - failOnStatusCode: false + failOnStatusCode: false, }).then((res) => { cy.wrap(res).as('accessTokenResponse') // expect(res.status).to.eq(200) @@ -244,47 +251,62 @@ Cypress.Commands.add('getServiceOrRouteID', (configType: string, host: string) = }).then((res) => { expect(res.status).to.eq(200) if (config === 'routes') { - cy.saveState(config + 'ID', Cypress._.get((Cypress._.filter(res.body.data, ["hosts", [host + ".api.gov.bc.ca"]]))[0], 'id')) - } - else { - cy.saveState(config + 'ID', Cypress._.get((Cypress._.filter(res.body.data, ["name", host]))[0], 'id')) + cy.saveState( + config + 'ID', + Cypress._.get( + Cypress._.filter(res.body.data, ['hosts', [host + '.api.gov.bc.ca']])[0], + 'id' + ) + ) + } else { + cy.saveState( + config + 'ID', + Cypress._.get(Cypress._.filter(res.body.data, ['name', host])[0], 'id') + ) } }) }) -Cypress.Commands.add('publishApi', (fileNames: any, namespace: string, flag?: boolean) => { - let fixtureFile = flag ? "state/regen" : "state/store"; - cy.log('< Publish API') - let fileName = '' - if (fileNames instanceof Array) { - for (const filepath of fileNames) { - fileName = fileName + ' ./cypress/fixtures/' + filepath; +Cypress.Commands.add( + 'publishApi', + (fileNames: any, namespace: string, flag?: boolean) => { + let fixtureFile = flag ? 'state/regen' : 'state/store' + cy.log('< Publish API') + let fileName = '' + if (fileNames instanceof Array) { + for (const filepath of fileNames) { + fileName = fileName + ' ./cypress/fixtures/' + filepath + } + } else { + fileName = ' ./cypress/fixtures/' + fileNames } - } - else { - fileName = ' ./cypress/fixtures/' + fileNames - } - const requestName: string = 'publishAPI' - cy.fixture(fixtureFile).then((creds: any) => { - const serviceAcctCreds = JSON.parse(creds.credentials) - cy.getAccessToken(serviceAcctCreds.clientId, serviceAcctCreds.clientSecret).then( - () => { - cy.wait(3000) - cy.get('@accessTokenResponse').then((res: any) => { - cy.executeCliCommand('gwa config set --namespace ' + namespace).then((response) => { - cy.executeCliCommand('gwa config set --token ' + res.body.access_token).then((response) => { - { - expect(response.stdout).to.contain("Config settings saved") - cy.executeCliCommand('gwa pg ' + fileName).then((response) => { - expect(response.stdout).to.contain("Gateway config published") + const requestName: string = 'publishAPI' + cy.fixture(fixtureFile).then((creds: any) => { + const serviceAcctCreds = JSON.parse(creds.credentials) + cy.getAccessToken(serviceAcctCreds.clientId, serviceAcctCreds.clientSecret).then( + () => { + cy.wait(3000) + cy.get('@accessTokenResponse').then((res: any) => { + cy.executeCliCommand('gwa config set --namespace ' + namespace).then( + (response) => { + cy.executeCliCommand( + 'gwa config set --token ' + res.body.access_token + ).then((response) => { + { + expect(response.stdout).to.contain('Config settings saved') + cy.executeCliCommand('gwa pg ' + fileName).then((response) => { + expect(response.stdout).to.contain('Gateway config published') + }) + } }) } - }) + ) }) - }) - }) - }) -}) + } + ) + }) + } +) Cypress.Commands.add('deleteAllCookies', () => { cy.clearCookies() @@ -293,120 +315,133 @@ Cypress.Commands.add('deleteAllCookies', () => { cy.clearCookie('keystone.sid') cy.clearCookie('_oauth2_proxy') cy.exec('npm cache clear --force') - var cookies = document.cookie.split(";"); + var cookies = document.cookie.split(';') for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i]; - var eqPos = cookie.indexOf("="); - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + var cookie = cookies[i] + var eqPos = cookie.indexOf('=') + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT' } }) -Cypress.Commands.add('makeKongRequest', (serviceName: string, methodType: string, key?: string) => { - let authorization - cy.fixture('state/regen').then((creds: any) => { - cy.wait(5000) - let token = key - if (key == undefined) { - token = creds.apikey +Cypress.Commands.add( + 'makeKongRequest', + (serviceName: string, methodType: string, key?: string) => { + let authorization + cy.fixture('state/regen').then((creds: any) => { + cy.wait(5000) + let token = key + if (key == undefined) { + token = creds.apikey + } + const service = serviceName + cy.log('Token->' + token) + return cy.request({ + url: Cypress.env('KONG_URL'), + method: methodType, + headers: { 'x-api-key': `${token}`, Host: `${service}` + '.api.gov.bc.ca' }, + failOnStatusCode: false, + auth: { + bearer: token, + }, + }) + }) + } +) + +Cypress.Commands.add( + 'makeKongGatewayRequest', + (endpoint: string, requestName: string, methodType: string) => { + let body = {} + var serviceEndPoint = endpoint + body = config[requestName] + if (requestName == '') { + body = {} } - const service = serviceName - cy.log("Token->" + token) return cy.request({ - url: Cypress.env('KONG_URL'), + url: Cypress.env('KONG_CONFIG_URL') + '/' + serviceEndPoint, method: methodType, - headers: { 'x-api-key': `${token}`, 'Host': `${service}` + '.api.gov.bc.ca' }, + body: body, + form: true, failOnStatusCode: false, - auth: { - bearer: token, - } }) - }) -}) - -Cypress.Commands.add('makeKongGatewayRequest', (endpoint: string, requestName: string, methodType: string) => { - let body = {} - var serviceEndPoint = endpoint - body = config[requestName] - if (requestName == '') { - body = {} } - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + serviceEndPoint, - method: methodType, - body: body, - form: true, - failOnStatusCode: false - }) -}) - -Cypress.Commands.add('makeKongGatewayRequestUsingClientIDSecret', (hostURL: string, methodType = 'GET') => { - cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { - - let cc = JSON.parse(store_res.clientidsecret) - - cy.getAccessToken(cc.clientId, cc.clientSecret).then(() => { - cy.get('@accessTokenResponse').then((token_res: any) => { - let token = token_res.body.access_token - cy.request({ - method: methodType, - url: Cypress.env('KONG_URL'), - failOnStatusCode: false, - headers: { - Host: hostURL, - }, - auth: { - bearer: token, - }, +) + +Cypress.Commands.add( + 'makeKongGatewayRequestUsingClientIDSecret', + (hostURL: string, methodType = 'GET') => { + cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { + let cc = JSON.parse(store_res.clientidsecret) + + cy.getAccessToken(cc.clientId, cc.clientSecret).then(() => { + cy.get('@accessTokenResponse').then((token_res: any) => { + let token = token_res.body.access_token + cy.request({ + method: methodType, + url: Cypress.env('KONG_URL'), + failOnStatusCode: false, + headers: { + Host: hostURL, + }, + auth: { + bearer: token, + }, + }) }) }) }) - }) -}) - -Cypress.Commands.add('updateKongPlugin', (pluginName: string, name: string, endPoint?: string, verb = 'POST') => { - cy.fixture('state/store').then((creds: any) => { - let body = {} - const pluginID = pluginName.toLowerCase() + 'id' - const id = creds[pluginID] - let endpoint - if (pluginName == '') - endpoint = 'plugins' - else if (id !== undefined) - endpoint = pluginName.toLowerCase() + '/' + id.toString() + '/' + 'plugins' - endpoint = (typeof endPoint !== 'undefined') ? endPoint : endpoint - body = config[name] - cy.log("Body->" + body) - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + endpoint, - method: verb, - body: body, - form: true, - failOnStatusCode: false + } +) + +Cypress.Commands.add( + 'updateKongPlugin', + (pluginName: string, name: string, endPoint?: string, verb = 'POST') => { + cy.fixture('state/store').then((creds: any) => { + let body = {} + const pluginID = pluginName.toLowerCase() + 'id' + const id = creds[pluginID] + let endpoint + if (pluginName == '') endpoint = 'plugins' + else if (id !== undefined) + endpoint = pluginName.toLowerCase() + '/' + id.toString() + '/' + 'plugins' + endpoint = typeof endPoint !== 'undefined' ? endPoint : endpoint + body = config[name] + cy.log('Body->' + body) + return cy.request({ + url: Cypress.env('KONG_CONFIG_URL') + '/' + endpoint, + method: verb, + body: body, + form: true, + failOnStatusCode: false, + }) }) - }) -}) - -Cypress.Commands.add('updateKongPluginForJSONRequest', (jsonBody: string, endPoint: string, verb = 'POST') => { - cy.fixture('state/store').then((creds: any) => { - let body = {} - let headers = { "content-type": "application/json", "accept": "application/json" } - body = jsonBody - return cy.request({ - url: Cypress.env('KONG_CONFIG_URL') + '/' + endPoint, - method: verb, - body: body, - headers: headers, - failOnStatusCode: false + } +) + +Cypress.Commands.add( + 'updateKongPluginForJSONRequest', + (jsonBody: string, endPoint: string, verb = 'POST') => { + cy.fixture('state/store').then((creds: any) => { + let body = {} + let headers = { 'content-type': 'application/json', accept: 'application/json' } + body = jsonBody + return cy.request({ + url: Cypress.env('KONG_CONFIG_URL') + '/' + endPoint, + method: verb, + body: body, + headers: headers, + failOnStatusCode: false, + }) }) - }) -}) + } +) -Cypress.Commands.add("generateKeystore", async () => { +Cypress.Commands.add('generateKeystore', async () => { let keyStore = jose.JWK.createKeyStore() await keyStore.generate('RSA', 2048, { alg: 'RS256', use: 'sig' }) return JSON.stringify(keyStore.toJSON(true), null, ' ') -}); +}) Cypress.Commands.add('setHeaders', (headerValues: any) => { headers = headerValues @@ -417,31 +452,58 @@ Cypress.Commands.add('setRequestBody', (body: any) => { }) Cypress.Commands.add('setAuthorizationToken', (token: string) => { - headers["Authorization"] = "Bearer " + token - headers = headers + headers['Authorization'] = 'Bearer ' + token +}) + +Cypress.Commands.add('callAPI', (endPoint: string, methodType: string) => { + let body = '{}' + let requestData: any = {} + if (methodType.toUpperCase() === 'PUT' || methodType.toUpperCase() === 'POST') { + body = requestBody + } + + requestData['appname'] = 'Test1' + requestData['url'] = Cypress.env('BASE_URL') + '/' + endPoint + requestData['headers'] = headers + requestData['body'] = '' + requestData['method'] = methodType + + cy.request({ + url: Cypress.env('BASE_URL') + '/' + endPoint, + method: methodType, + body, + headers, + failOnStatusCode: false, + }).then((apiResponse) => { + // You can also return data or use it in further tests + const responseData = { + apiRes: apiResponse, + } + // cy.addToAstraScanIdList(response2.body.status) + return responseData + }) }) Cypress.Commands.add('makeAPIRequest', (endPoint: string, methodType: string) => { - let body = {} let requestData: any = {} if (methodType.toUpperCase() === 'PUT' || methodType.toUpperCase() === 'POST') { body = requestBody } - requestData['appname'] = "Test1" + requestData['appname'] = 'Test1' requestData['url'] = Cypress.env('BASE_URL') + '/' + endPoint requestData['headers'] = headers - requestData['body'] = "" + requestData['body'] = '' requestData['method'] = methodType - + // Scan request with Astra cy.request({ url: 'http://astra.localtest.me:8094/scan/', method: 'POST', body: requestData, headers: headers, - failOnStatusCode: false + failOnStatusCode: false, }).then((astraResponse) => { // Actual API request cy.request({ @@ -449,24 +511,24 @@ Cypress.Commands.add('makeAPIRequest', (endPoint: string, methodType: string) => method: methodType, body: body, headers: headers, - failOnStatusCode: false + failOnStatusCode: false, }).then((apiResponse) => { // You can also return data or use it in further tests const responseData = { astraRes: astraResponse, apiRes: apiResponse, - }; + } // cy.addToAstraScanIdList(response2.body.status) - return responseData; + return responseData }) - }); + }) }) Cypress.Commands.add('makeAPIRequestForScanResult', (scanID: string) => { return cy.request({ - url: 'http://astra.localtest.me:8094/alerts/' + scanID, + url: 'http://astra.localtest.me:8094/alerts/' + scanID, method: 'GET', - failOnStatusCode: false + failOnStatusCode: false, }) }) @@ -478,151 +540,168 @@ Cypress.Commands.add('selectLoginOptions', (username: string) => { cy.get(login.loginDropDown).click() if (username.includes('idir')) { login.selectAPIProviderLoginOption() - } - else { + } else { login.selectDeveloperLoginOption() } }) Cypress.Commands.add('verifyToastMessage', (msg: string) => { - cy.get('[role="alert"]', { timeout: 2000 }).closest('div').invoke('text') + cy.get('[role="alert"]', { timeout: 2000 }) + .closest('div') + .invoke('text') .then((text) => { - const toastText = text; - expect(toastText).to.contain(msg); + const toastText = text + expect(toastText).to.contain(msg) }) }) -Cypress.Commands.add('compareJSONObjects', (actualResponse: any, expectedResponse: any, indexFlag = false) => { - let response = actualResponse - if (indexFlag) { - const index = actualResponse.findIndex((x: { name: string }) => x.name === expectedResponse.name); - response = actualResponse[index] - } - for (var p in expectedResponse) { - if (typeof (expectedResponse[p]) === "object") { - var objectValue1 = expectedResponse[p], - objectValue2 = response[p]; - for (var value in objectValue1) { - if (!(['activityAt', 'id'].includes(value))) { - cy.compareJSONObjects(objectValue2[value], objectValue1[value]); +Cypress.Commands.add( + 'compareJSONObjects', + (actualResponse: any, expectedResponse: any, indexFlag = false) => { + let response = actualResponse + if (indexFlag) { + const index = actualResponse.findIndex( + (x: { name: string }) => x.name === expectedResponse.name + ) + response = actualResponse[index] + } + for (var p in expectedResponse) { + if (typeof expectedResponse[p] === 'object') { + var objectValue1 = expectedResponse[p], + objectValue2 = response[p] + for (var value in objectValue1) { + if (!['activityAt', 'id'].includes(value)) { + cy.compareJSONObjects(objectValue2[value], objectValue1[value]) + } + } + } else { + if (expectedResponse[p] == 'true' || expectedResponse[p] == 'false') + Boolean(expectedResponse[p]) + if (['organization', 'organizationUnit'].includes(p) && !indexFlag) { + response[p] = response[p]['name'] + } + if ( + response[p] !== expectedResponse[p] && + !['clientSecret', 'appId', 'isInCatalog', 'isDraft', 'consumer', 'id'].includes( + p + ) + ) { + cy.log('Different Value ->' + expectedResponse[p]) + assert.fail('JSON value mismatch for ' + p) } - } - } else { - if ((expectedResponse[p] == 'true') || (expectedResponse[p] == 'false')) - Boolean(expectedResponse[p]) - if (['organization', 'organizationUnit'].includes(p) && (!indexFlag)) { - response[p] = response[p]['name'] - } - if ((response[p] !== expectedResponse[p]) && !(['clientSecret', 'appId', 'isInCatalog', 'isDraft', 'consumer', 'id'].includes(p))) { - cy.log("Different Value ->" + expectedResponse[p]) - assert.fail("JSON value mismatch for " + p) } } } -}) +) + +Cypress.Commands.add( + 'updatePluginFile', + (filename: string, serviceName: string, pluginFileName: string) => { + cy.readFile('cypress/fixtures/' + pluginFileName).then(($el) => { + let newObj: any + newObj = YAML.parse($el) + cy.readFile('cypress/fixtures/' + filename).then((content: any) => { + let obj = YAML.parse(content) + const keys = Object.keys(obj) + Object.keys(obj.services).forEach(function (key, index) { + if (obj.services[index].name == serviceName) { + obj.services[index].plugins = newObj.plugins + } + }) + const yamlString = YAML.stringify(obj, 'utf8') + cy.writeFile('cypress/fixtures/' + filename, yamlString) + }) + }) + } +) -Cypress.Commands.add('updatePluginFile', (filename: string, serviceName: string, pluginFileName: string) => { - cy.readFile('cypress/fixtures/' + pluginFileName).then(($el) => { - let newObj: any - newObj = YAML.parse($el) +Cypress.Commands.add( + 'updatePropertiesOfPluginFile', + (filename: string, propertyName: any, propertyValue: any) => { cy.readFile('cypress/fixtures/' + filename).then((content: any) => { let obj = YAML.parse(content) - const keys = Object.keys(obj); - Object.keys(obj.services).forEach(function (key, index) { - if (obj.services[index].name == serviceName) { - obj.services[index].plugins = newObj.plugins - } - }); - const yamlString = YAML.stringify(obj, 'utf8'); + const keys = Object.keys(obj) + if (propertyName === 'config.anonymous') { + obj.plugins[0].config.anonymous = propertyValue + } else if (propertyName === 'tags') { + obj.plugins[0][propertyName] = propertyValue + } else { + Object.keys(obj.services).forEach(function (key, index) { + if (propertyName == 'methods') { + obj.services[0].routes[0].methods = propertyValue + } else { + obj.services[0].plugins[0].config[propertyName] = propertyValue + } + }) + } + const yamlString = YAML.stringify(obj, 'utf8') cy.writeFile('cypress/fixtures/' + filename, yamlString) }) - }) -}) + } +) +Cypress.Commands.add( + 'getTokenUsingJWKCredentials', + (credential: any, privateKey: any) => { + let jwkCred = JSON.parse(credential) + let clientId = jwkCred.clientId + let tokenEndpoint = jwkCred.tokenEndpoint + let now = Math.floor(new Date().getTime() / 1000) + let plus5Minutes = new Date((now + 5 * 60) * 1000) + let alg = 'RS256' -Cypress.Commands.add('updatePropertiesOfPluginFile', (filename: string, propertyName: any, propertyValue: any) => { - cy.readFile('cypress/fixtures/' + filename).then((content: any) => { - let obj = YAML.parse(content) - const keys = Object.keys(obj); - if (propertyName === "config.anonymous") { - obj.plugins[0].config.anonymous = propertyValue + let claims = { + aud: Cypress.env('OIDC_ISSUER'), } - else if (propertyName === "tags") { - obj.plugins[0][propertyName] = propertyValue - } - else { - Object.keys(obj.services).forEach(function (key, index) { - if (propertyName == "methods") { - obj.services[0].routes[0].methods = propertyValue - } - else { - obj.services[0].plugins[0].config[propertyName] = propertyValue - } - }); - } - const yamlString = YAML.stringify(obj, 'utf8'); - cy.writeFile('cypress/fixtures/' + filename, yamlString) - }) -}) - -Cypress.Commands.add('getTokenUsingJWKCredentials', (credential: any, privateKey: any) => { - let jwkCred = JSON.parse(credential) - let clientId = jwkCred.clientId - let tokenEndpoint = jwkCred.tokenEndpoint - let now = Math.floor(new Date().getTime() / 1000) - let plus5Minutes = new Date((now + 5 * 60) * 1000) - let alg = 'RS256' + let jwt = njwt + .create(claims, privateKey, alg) + .setIssuedAt(now) + .setExpiration(plus5Minutes) + .setIssuer(clientId) + .setSubject(clientId) + .compact() - let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', + cy.request({ + url: tokenEndpoint, + method: 'POST', + body: { + grant_type: 'client_credentials', + client_id: clientId, + scopes: 'openid', + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: jwt, + }, + form: true, + }) } +) - let jwt = njwt - .create(claims, privateKey, alg) - .setIssuedAt(now) - .setExpiration(plus5Minutes) - .setIssuer(clientId) - .setSubject(clientId) - .compact() - - cy.request({ - url: tokenEndpoint, - method: 'POST', - body: { - grant_type: 'client_credentials', - client_id: clientId, - scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwt, - }, - form: true, - }) -}) - -Cypress.Commands.add("generateKeyPair", () => { - const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); +Cypress.Commands.add('generateKeyPair', () => { + const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }) // Convert the key pair to PEM format - const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey); - const publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey); + const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey) + const publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey) cy.writeFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem', privateKeyPem) cy.writeFile('cypress/fixtures/state/jwtReGenPublicKey_new.pub', publicKeyPem) - }) Cypress.Commands.add('forceVisit', (url: string) => { - cy.window().then(win => { - return win.open(url, '_self'); - }); -}); - -Cypress.Commands.add('updateJsonBoby', (json: any, key: string, newValue: string): any => { - json[key] = newValue - return json -}); + cy.window().then((win) => { + return win.open(url, '_self') + }) +}) + +Cypress.Commands.add( + 'updateJsonBoby', + (json: any, key: string, newValue: string): any => { + json[key] = newValue + return json + } +) const formDataRequest = ( options: formDataRequestOptions, diff --git a/e2e/cypress/support/global.d.ts b/e2e/cypress/support/global.d.ts index 66c452b27..1f3ce3aa9 100644 --- a/e2e/cypress/support/global.d.ts +++ b/e2e/cypress/support/global.d.ts @@ -21,11 +21,19 @@ declare namespace Cypress { makeKongRequest(serviceName: string, methodType: string, key?: string): Chainable - makeKongGatewayRequestUsingClientIDSecret(hostURL: string, methodType?: string): Chainable + makeKongGatewayRequestUsingClientIDSecret( + hostURL: string, + methodType?: string + ): Chainable preserveCookiesDefaults(): void - saveState(key: string, value: string, flag?: boolean, isGlobal?: boolean): Chainable + saveState( + key: string, + value: string, + flag?: boolean, + isGlobal?: boolean + ): Chainable getState(key: string): Chainable @@ -36,14 +44,29 @@ declare namespace Cypress { client_secret: string ): Chainable> - publishApi(fileNames: any, namespace: string, flag?: boolean): Chainable> + publishApi( + fileNames: any, + namespace: string, + flag?: boolean + ): Chainable> - getServiceOrRouteID(configType: string, host: string + getServiceOrRouteID( + configType: string, + host: string ): Chainable> - updateKongPlugin(pluginName: string, name: string, endPoint?: string, verb?: string): Chainable> + updateKongPlugin( + pluginName: string, + name: string, + endPoint?: string, + verb?: string + ): Chainable> - makeKongGatewayRequest(endpoint: string, requestName: string, methodType: string): Chainable> + makeKongGatewayRequest( + endpoint: string, + requestName: string, + methodType: string + ): Chainable> // generateKeystore() : Chainable @@ -55,25 +78,49 @@ declare namespace Cypress { setAuthorizationToken(token: string): void + callAPI(endPoint: string, methodType: string): Chainable> + makeAPIRequest(endPoint: string, methodType: string): Chainable> getUserSession(): Chainable> - compareJSONObjects(actualResponse: any, expectedResponse: any, indexFlag?: boolean): Chainable> + compareJSONObjects( + actualResponse: any, + expectedResponse: any, + indexFlag?: boolean + ): Chainable> - getUserSessionTokenValue(namespace: string, isNamespaceSelected?: boolean): Chainable> + getUserSessionTokenValue( + namespace: string, + isNamespaceSelected?: boolean + ): Chainable> getUserSessionResponse(): Chainable> - getTokenUsingJWKCredentials(credential: any, privateKey: any): Chainable> + getTokenUsingJWKCredentials( + credential: any, + privateKey: any + ): Chainable> verifyToastMessage(msg: string): Chainable> - updatePluginFile(filename: string, serviceName: string, pluginFileName: string): Chainable> + updatePluginFile( + filename: string, + serviceName: string, + pluginFileName: string + ): Chainable> - updateElementsInPluginFile(filename: string, elementName: string, elementValue: string): Chainable> + updateElementsInPluginFile( + filename: string, + elementName: string, + elementValue: string + ): Chainable> - updatePropertiesOfPluginFile(filename: string, propertyName: any, propertyValue: any): Chainable> + updatePropertiesOfPluginFile( + filename: string, + propertyName: any, + propertyValue: any + ): Chainable> keycloakLogin(username: string, password: string): Chainable @@ -84,27 +131,44 @@ declare namespace Cypress { generateKeyPair(): void // isProductDisplay(productName: string, expResult : boolean) :Chainable> - updateJsonValue(filePath: string, jsonPath: string, newValue: string, index?: any): Chainable - - updateKongPluginForJSONRequest(jsonBody: string, endPoint: string, verb?: string): Chainable> + updateJsonValue( + filePath: string, + jsonPath: string, + newValue: string, + index?: any + ): Chainable + + updateKongPluginForJSONRequest( + jsonBody: string, + endPoint: string, + verb?: string + ): Chainable> forceVisit(url: string): Chainable executeCliCommand(command: string): Chainable - replaceWordInJsonObject(targetWord: string, replacement: string, fileName: string): Chainable> + replaceWordInJsonObject( + targetWord: string, + replacement: string, + fileName: string + ): Chainable> gwaPublish(type: string, fileName: string): Chainable> - replaceWord(originalString: string, wordToReplace: string, replacementWord: string): Chainable + replaceWord( + originalString: string, + wordToReplace: string, + replacementWord: string + ): Chainable + + updateJsonBoby(json: any, key: string, newValue: string): Chainable - updateJsonBoby(json: any, key: string, newValue: string):Chainable + deleteFileInE2EFolder(fileName: string): Chainable - deleteFileInE2EFolder(fileName: string):Chainable + addToAstraScanIdList(item: any): Chainable - addToAstraScanIdList(item: any):Chainable - - checkAstraScanResultForVulnerability():Chainable + checkAstraScanResultForVulnerability(): Chainable makeAPIRequestForScanResult(scanID: string): Chainable> } diff --git a/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts b/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts index 6bc97f8ee..9a10b7965 100644 --- a/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts +++ b/e2e/cypress/tests/02-client-credential-flow/10-jwt-genkp-access-approve-api-rqst.cy.ts @@ -60,7 +60,7 @@ describe('Make an API request using JWT signed with private key', () => { let alg = 'RS256' let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', + aud: Cypress.env('OIDC_ISSUER'), } let jwt = njwt @@ -78,7 +78,8 @@ describe('Make an API request using JWT signed with private key', () => { grant_type: 'client_credentials', client_id: clientId, scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion_type: + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: jwt, }, form: true, diff --git a/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts b/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts index f9ee17ad9..8fdd22d96 100644 --- a/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts +++ b/e2e/cypress/tests/02-client-credential-flow/14-jwt-publlicKey-access-approve-api-rqst.cy.ts @@ -51,53 +51,56 @@ describe('Access manager approves developer access request for JWT - Generated K describe('Make an API request using JWT signed with private key', () => { it('Get access token using JWT key pair; make sure API calls successfully', () => { cy.readFile('cypress/fixtures/state/store.json').then((store_res) => { - cy.readFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem').then((privateKey) => { - let jwkCred = JSON.parse(store_res.jwksurlcredentials) - let clientId = jwkCred.clientId - let tokenEndpoint = jwkCred.tokenEndpoint + cy.readFile('cypress/fixtures/state/jwtReGenPrivateKey_new.pem').then( + (privateKey) => { + let jwkCred = JSON.parse(store_res.jwksurlcredentials) + let clientId = jwkCred.clientId + let tokenEndpoint = jwkCred.tokenEndpoint - let now = Math.floor(new Date().getTime() / 1000) - let plus5Minutes = new Date((now + 5 * 60) * 1000) - let alg = 'RS256' + let now = Math.floor(new Date().getTime() / 1000) + let plus5Minutes = new Date((now + 5 * 60) * 1000) + let alg = 'RS256' - let claims = { - aud: Cypress.env('OIDC_ISSUER') + '/auth/realms/master', - } + let claims = { + aud: Cypress.env('OIDC_ISSUER'), + } - let jwt = njwt - .create(claims, privateKey, alg) - .setIssuedAt(now) - .setExpiration(plus5Minutes) - .setIssuer(clientId) - .setSubject(clientId) - .compact() + let jwt = njwt + .create(claims, privateKey, alg) + .setIssuedAt(now) + .setExpiration(plus5Minutes) + .setIssuer(clientId) + .setSubject(clientId) + .compact() - cy.request({ - url: tokenEndpoint, - method: 'POST', - body: { - grant_type: 'client_credentials', - client_id: clientId, - scopes: 'openid', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwt, - }, - form: true, - }).then((res) => { - let token = res.body.access_token cy.request({ - url: Cypress.env('KONG_URL'), - headers: { - Host: 'cc-service-for-platform.api.gov.bc.ca', - }, - auth: { - bearer: token, + url: tokenEndpoint, + method: 'POST', + body: { + grant_type: 'client_credentials', + client_id: clientId, + scopes: 'openid', + client_assertion_type: + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: jwt, }, + form: true, }).then((res) => { - expect(res.status).to.eq(200) + let token = res.body.access_token + cy.request({ + url: Cypress.env('KONG_URL'), + headers: { + Host: 'cc-service-for-platform.api.gov.bc.ca', + }, + auth: { + bearer: token, + }, + }).then((res) => { + expect(res.status).to.eq(200) + }) }) - }) - }) + } + ) }) }) }) diff --git a/e2e/cypress/tests/19-api-v3/api-suite.ts b/e2e/cypress/tests/19-api-v3/api-suite.ts new file mode 100644 index 000000000..d76f6ecc8 --- /dev/null +++ b/e2e/cypress/tests/19-api-v3/api-suite.ts @@ -0,0 +1,795 @@ +/** + * Prerequisites: + * - Organization and Organization Unit (ministry-of-health) exists + */ + +const { v4: uuidv4 } = require('uuid') + +function buildGatewayDatasetAndProduct() { + const datasetId = uuidv4().replace(/-/g, '').toUpperCase().substring(0, 4) + + cy.fixture('api-v3').as('api-v3') + return cy.loginByAuthAPI('', '').then(() => { + return cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + + // just reference the dataset id for easier tracing + const orgId = datasetId + + const org = { + name: `ministry-of-kittens-${orgId}`, + title: 'Some good title about kittens', + description: 'Some good description about kittens', + tags: [], + orgUnits: [ + { + name: `division-of-toys-${orgId}`, + title: 'Division of fun toys to play', + description: 'Some good description about how we manage our toys', + tags: [], + extForeignKey: `division-of-toys-${orgId}`, + extSource: 'internal', + extRecordHash: '', + }, + ], + extSource: 'internal', + extRecordHash: '', + } + + cy.setRequestBody(org) + return cy + .callAPI('ds/api/v3/organizations/ca.bc.gov', 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const orgAccess = { + name: org.name, + parent: `/ca.bc.gov`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgAccess) + + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const orgUnitAccess = { + name: org.orgUnits[0].name, + parent: `/ca.bc.gov/${org.name}`, + members: [ + { + member: { + email: 'janis@testmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(orgUnitAccess) + + cy.callAPI(`ds/api/v3/organizations/ca.bc.gov/access`, 'PUT').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + } + ) + + const payload = { + name: `org-dataset-${datasetId}`, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + tags: ['tag1', 'tag2'], + organization: org.name, + organizationUnit: org.orgUnits[0].name, + } + cy.setRequestBody(payload) + return cy + .callAPI(`ds/api/v3/organizations/${org.name}/datasets`, 'PUT') + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { status: 200, result: 'created', childResults: [] } + const datasetId = body.id + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return cy + .callAPI( + `ds/api/v3/organizations/${org.name}/datasets/${payload.name}`, + 'GET' + ) + .then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: payload.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + cy.setRequestBody({}) + return cy + .callAPI('ds/api/v3/gateways', 'POST') + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + cy.callAPI( + `ds/api/v3/organizations/${org.name}/${org.orgUnits[0].name}/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + + const product = { + name: `my-product-on-${myGateway.gatewayId}`, + dataset: payload.name, + environments: [ + { + name: 'dev', + active: true, + approval: false, + flow: 'public', + }, + ], + } + cy.setRequestBody(product) + + return cy + .callAPI( + `ds/api/v3/gateways/${myGateway.gatewayId}/products`, + 'PUT' + ) + .then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + + const match = { + status: 200, + result: 'created', + childResults: [], + } + delete body.id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + + return { + org, + gateway: myGateway, + dataset: payload, + datasetId, + product, + } + }) + }) + }) + }) + }) + }) + }) +} + +describe('API Directory', () => { + let workingData: any + + before(() => { + buildGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('PUT /organizations/{org}/datasets', () => { + const { org, gateway, dataset, datasetId, product } = workingData + + cy.callAPI(`ds/api/v3/directory/${datasetId}`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: `my-product-on-${gateway.gatewayId}`, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + delete body.products[0].id + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /directory', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + expect(body.length).to.be.greaterThan(0) + }) + }) + + it('GET /directory/{id}', () => { + cy.callAPI('ds/api/v3/directory', 'GET').then(({ apiRes: { status, body } }: any) => { + cy.log(`Directory ${JSON.stringify(body, null, 4)}`) + expect(status).to.be.equal(200) + }) + }) +}) + +describe('API Directory (Gateway Management)', () => { + let workingData: any + + before(() => { + buildGatewayDatasetAndProduct().then((data) => { + workingData = data + }) + }) + + it('GET /gateways/{gatewayId}/datasets/{name}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/datasets/${dataset.name}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + download_audience: 'Public', + record_publish_date: '2017-09-05', + notes: 'Some notes', + title: 'A title about my dataset', + isInCatalog: false, + isDraft: true, + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + tags: [], + description: 'Some good description about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + tags: [], + description: 'Some good description about how we manage our toys', + }, + } + + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + + it('GET /gateways/{gatewayId}/directory', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI(`ds/api/v3/gateways/${gateway.gatewayId}/directory`, 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = [ + { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + view_audience: 'Public', + security_class: 'PUBLIC', + record_publish_date: '2017-09-05', + tags: ['tag1', 'tag2'], + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public' }], + }, + ], + }, + ] + + delete body[0].products[0].id + delete body[0].id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /gateways/{gatewayId}/directory/{id}', () => { + const { org, gateway, dataset, datasetId, product } = workingData + cy.callAPI( + `ds/api/v3/gateways/${gateway.gatewayId}/directory/${datasetId}`, + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: dataset.name, + title: 'A title about my dataset', + notes: 'Some notes', + license_title: 'Open Government Licence - British Columbia', + security_class: 'PUBLIC', + view_audience: 'Public', + tags: ['tag1', 'tag2'], + record_publish_date: '2017-09-05', + isInCatalog: false, + organization: { + name: org.name, + title: 'Some good title about kittens', + }, + organizationUnit: { + name: org.orgUnits[0].name, + title: 'Division of fun toys to play', + }, + products: [ + { + name: product.name, + environments: [{ name: 'dev', active: true, flow: 'public', services: [] }], + }, + ], + } + + delete body.products[0].id + delete body.id + + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) + +describe('Organization', () => { + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + }) + }) + }) + + it('GET /organizations', () => { + cy.callAPI('ds/api/v3/organizations', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.filter((o: any) => o.name == 'ministry-of-health').length + ).to.be.equal(1) + } + ) + }) + it('GET /organizations/{org}', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + body.orgUnits.filter((o: any) => o.name == 'public-health').length + ).to.be.equal(1) + } + ) + }) + + it('GET /organizations/{org}/roles', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + roles: [ + { + name: 'organization-admin', + permissions: [ + { + resource: 'org/ministry-of-health', + scopes: ['Dataset.Manage', 'GroupAccess.Manage', 'Namespace.Assign'], + }, + ], + }, + ], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/roles', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/access', () => { + const match = { + name: 'ministry-of-health', + parent: '/ca.bc.gov', + members: [], + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/access', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/access', () => { + const payload = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + cy.setRequestBody(payload) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(204) + + cy.callAPI( + 'ds/api/v3/organizations/planning-and-innovation-division/access', + 'GET' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + + const match = { + name: 'planning-and-innovation-division', + parent: '/ca.bc.gov/ministry-of-health', + members: [ + { + member: { + username: 'mark@idir', + email: 'mark@gmail.com', + }, + roles: ['organization-admin'], + }, + ], + } + // ignore the ID as it will always be different + body.members.forEach((m: any) => { + delete m.member.id + }) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) + }) + + it('GET /organizations/{org}/gateways', () => { + const match = { + name: 'platform', + orgUnit: 'planning-and-innovation-division', + enabled: false, + updatedAt: 0, + } + + cy.callAPI('ds/api/v3/organizations/ministry-of-health/gateways', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect( + JSON.stringify(body.filter((a: any) => a.name == 'platform').pop()) + ).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('GET /organizations/{org}/activity', () => { + cy.callAPI('ds/api/v3/organizations/ministry-of-health/activity', 'GET').then( + ({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + // expect(JSON.stringify(body.filter(a => a.params.ns == ))).to.be.equal(JSON.stringify(match)) + } + ) + }) + + it('PUT /organizations/{org}/{orgUnit}/gateways/{gatewayId}', () => { + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + const myGateway = body + + cy.setRequestBody({}) + cy.callAPI( + `ds/api/v3/organizations/ministry-of-health/planning-and-innovation-division/gateways/${myGateway.gatewayId}?enable=true`, + 'PUT' + ).then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(body.result).to.be.equal('namespace-assigned') + }) + }) + }) + + it('GET /roles', () => { + const match: any = { + 'organization-admin': { + label: 'Organization Administrator', + permissions: [ + { + resourceType: 'organization', + scopes: ['GroupAccess.Manage', 'Namespace.Assign', 'Dataset.Manage'], + }, + { resourceType: 'namespace', scopes: ['Namespace.View'] }, + ], + }, + } + + cy.callAPI('ds/api/v3/roles', 'GET').then(({ apiRes: { status, body } }: any) => { + expect(status).to.be.equal(200) + expect(JSON.stringify(body)).to.be.equal(JSON.stringify(match)) + }) + }) +}) + +describe('Gateways', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + }) + }) + }) + + it('POST /gateways', () => { + const payload = { + displayName: 'My ABC Gateway', + } + cy.setRequestBody(payload) + cy.callAPI('ds/api/v3/gateways', 'POST').then(({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(payload.displayName) + LOCAL.myGateway = body + }) + }) + + it('GET /gateways/{gatewayId}', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.displayName).to.be.equal(LOCAL.myGateway.displayName) + } + ) + }) + + it('GET /gateways/{gatewayId}/activity', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/activity`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].message).to.be.equal('{actor} created {ns} namespace') + expect(body[0].params.ns).to.be.equal(LOCAL.myGateway.gatewayId) + } + ) + }) + + // it('DELETE /gateways/{gatewayId}', () => { + // cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}`, 'DELETE').then( + // ({ apiRes: { body, status } }: any) => { + // expect(status).to.be.equal(200) + // cy.log(JSON.stringify(body, null, 2)) + // } + // ) + // }) +}) + +describe('Products', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + LOCAL.myGateway = body + } + ) + }) + }) + }) + + it('PUT /gateways/{gatewayId}/products', () => { + cy.setRequestBody({ + name: `my-product-on-${LOCAL.myGateway.gatewayId}`, + environments: [ + { + name: 'dev', + active: false, + approval: false, + flow: 'public', + }, + ], + }) + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/products', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/products`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + expect(body[0].name).to.be.equal(`my-product-on-${LOCAL.myGateway.gatewayId}`) + expect(body[0].environments.length).to.be.equal(1) + } + ) + }) +}) + +describe('Authorization Profiles', () => { + let LOCAL: { myGateway?: any } = {} + + before(() => { + cy.fixture('api-v3').as('api-v3') + cy.loginByAuthAPI('', '').then(() => { + cy.get('@loginByAuthApiResponse').then((token_res: any) => { + cy.setHeaders({ 'Content-Type': 'application/json' }) + cy.setAuthorizationToken(token_res.token) + cy.setRequestBody({}) + cy.callAPI('ds/api/v3/gateways', 'POST').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + LOCAL.myGateway = body + } + ) + }) + }) + }) + + it('PUT /gateways/{gatewayId}/issuers', () => { + cy.setRequestBody({ + name: `my-auth-profile-for-${LOCAL.myGateway.gatewayId}`, + description: 'Auth connection to my IdP', + flow: 'client-credentials', + clientAuthenticator: 'client-secret', + mode: 'auto', + inheritFrom: 'Sample Shared IdP', + }) + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'PUT').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body)) + } + ) + }) + + it('GET /gateways/{gatewayId}/issuers', () => { + cy.callAPI(`ds/api/v3/gateways/${LOCAL.myGateway.gatewayId}/issuers`, 'GET').then( + ({ apiRes: { body, status } }: any) => { + expect(status).to.be.equal(200) + cy.log(JSON.stringify(body, null, 2)) + expect(body.length).to.be.equal(1) + + const issuer = body[0] + + expect(issuer.name).to.be.equal( + `my-auth-profile-for-${LOCAL.myGateway.gatewayId}` + ) + expect(issuer.environmentDetails[0].environment).to.be.equal('test') + expect(issuer.environmentDetails[0].issuerUrl).to.be.equal( + Cypress.env('OIDC_ISSUER') + ) + expect(issuer.environmentDetails[0].clientId).to.be.equal( + `ap-my-auth-profile-for-${LOCAL.myGateway.gatewayId}-test` + ) + } + ) + }) +}) + +describe('Identifiers', () => { + it('GET /identifiers/application', () => { + cy.callAPI('ds/api/v3/identifiers/application', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/product', () => { + cy.callAPI('ds/api/v3/identifiers/product', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/environment', () => { + cy.callAPI('ds/api/v3/identifiers/environment', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) + + it('GET /identifiers/gateway', () => { + cy.callAPI('ds/api/v3/identifiers/gateway', 'GET').then( + ({ apiRes: { status, body } }: any) => { + cy.log(`ID ${body}`) + expect(status).to.be.equal(200) + } + ) + }) +}) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index da494f9c1..5c265fe6b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -26,6 +26,7 @@ "npm-run-all": "^4.1.5", "request": "^2.88.2", "typescript": "^4.3.5", + "uuid": "^8.3.2", "yaml": "^2.1.3", "yamljs": "^0.3.0" }, @@ -119,11 +120,11 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -131,28 +132,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -176,11 +177,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.24.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -190,12 +191,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -213,57 +214,61 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -273,78 +278,78 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -410,9 +415,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -421,31 +426,31 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -462,12 +467,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -875,9 +880,9 @@ } }, "node_modules/@types/node": { - "version": "16.18.97", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.97.tgz", - "integrity": "sha512-4muilE1Lbfn57unR+/nT9AFjWk0MtWi5muwCEJqnOvfRQDbSfLCUdN7vCIg8TYuaANfhLOV85ve+FNpiUsbSRg==", + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", "dev": true }, "node_modules/@types/request": { @@ -1884,6 +1889,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1968,9 +1974,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "version": "1.0.30001628", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001628.tgz", + "integrity": "sha512-S3BnR4Kh26TBxbi5t5kpbcUlLJb9lhtDXISDPwOfI+JoC+ik0QksvkZtUVyikw3hjnkgkMPSJ8oIM9yMm9vflA==", "funding": [ { "type": "opencollective", @@ -2386,9 +2392,9 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/cypress": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz", - "integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz", + "integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2844,9 +2850,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.779", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.779.tgz", - "integrity": "sha512-oaTiIcszNfySXVJzKcjxd2YjPxziAd+GmXyb2HbidCeFo6Z88ygOT7EimlrEQhM2U08VhSrbKhLOXP0kKUCZ6g==" + "version": "1.4.790", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.790.tgz", + "integrity": "sha512-eVGeQxpaBYbomDBa/Mehrs28MdvCXfJmEFzaMFsv8jH/MJDLIylJN81eTJ5kvx7B7p18OiPK0BkC06lydEy63A==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -3981,6 +3987,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7184,6 +7191,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -7684,9 +7692,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" }, "node_modules/sprintf-js": { "version": "1.1.3", @@ -8062,9 +8070,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/tsutils": { @@ -8646,9 +8654,9 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", + "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", "bin": { "yaml": "bin.mjs" }, diff --git a/e2e/package.json b/e2e/package.json index a88a6812c..c8c5664b1 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -64,6 +64,7 @@ "npm-run-all": "^4.1.5", "request": "^2.88.2", "typescript": "^4.3.5", + "uuid": "^8.3.2", "yaml": "^2.1.3", "yamljs": "^0.3.0" } diff --git a/local/keycloak/master-realm.json b/local/keycloak/master-realm.json index df8ae3277..80e884ff3 100644 --- a/local/keycloak/master-realm.json +++ b/local/keycloak/master-realm.json @@ -2071,7 +2071,7 @@ "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "config": { - "scopes": "[\"GroupAccess.Manage\",\"Namespace.Assign\"]", + "scopes": "[\"GroupAccess.Manage\",\"Namespace.Assign\",\"Dataset.Manage\"]", "applyPolicies": "[\"janis\"]" } }, @@ -2317,9 +2317,7 @@ } } ], - "defaultClientScopes": [ - "profile" - ], + "defaultClientScopes": ["profile"], "optionalClientScopes": [] }, { diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index 7f9decf70..ad837461c 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -80,7 +80,12 @@ export function expressAuthentication( request, { status: (s: number) => { - logger.error('invalid_token (%d) for %j', s, request.oauth_user); + logger.error( + 'invalid_token (%d) [%j] for %j', + s, + permissions, + request.oauth_user + ); reject( new UnauthorizedError('invalid_token', { message: `Missing authorization scope. (${s})`, diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 40f87b964..6dea337a9 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -15,7 +15,7 @@ const metadata = { transformations: { tags: { name: 'toStringDefaultArray' }, orgUnits: { - name: 'connectExclusiveList', + name: 'connectExclusiveListCreate', list: 'OrganizationUnit', syncFirst: true, }, diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index 2d0ce490a..f82c4eeaa 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -677,6 +677,17 @@ export const removeKeys = (obj: object, keys: string[]) => { return obj; }; +export const replaceKey = (obj: object, oldKey: string, newKey: string) => { + Object.entries(obj).forEach( + ([key, val]) => + (oldKey == key && + delete (obj as any)[key] && + ((obj as any)[newKey] = val)) || + (val && typeof val === 'object' && replaceKey(val, oldKey, newKey)) + ); + return obj; +}; + export const removeAllButKeys = (obj: object, keys: string[]) => { Object.entries(obj).forEach( ([key, val]) => diff --git a/src/controllers/ioc/keystoneInjector.ts b/src/controllers/ioc/keystoneInjector.ts index a29f9dd4c..1da31cf8d 100644 --- a/src/controllers/ioc/keystoneInjector.ts +++ b/src/controllers/ioc/keystoneInjector.ts @@ -47,7 +47,7 @@ export class KeystoneService { id: null, name: resolveName(request.user), username: resolveUsername(request.user), - namespace: request.params.ns, + namespace: request.params.ns || request.params.gatewayId, roles: JSON.stringify(scopesToRoles(identityProvider, _scopes)), scopes: _scopes, userId: null, diff --git a/src/controllers/v3/GatewayController.ts b/src/controllers/v3/GatewayController.ts index d44802a2d..863ea21bb 100644 --- a/src/controllers/v3/GatewayController.ts +++ b/src/controllers/v3/GatewayController.ts @@ -36,7 +36,7 @@ import { getActivity } from '../../services/keystone/activity'; import { transformActivity } from '../../services/workflow'; import { ActivityDetail } from './types-extra'; -const logger = Logger('controllers.Namespace'); +const logger = Logger('controllers.Gateway'); /** * @param binary Buffer @@ -152,8 +152,9 @@ export class NamespaceController extends Controller { @Security('jwt', []) public async create( @Request() request: any, - @Body() vars: NamespaceInput + @Body() vars: Gateway ): Promise { + logger.debug('Input %j', vars); const result = await this.keystone.executeGraphQL({ context: this.keystone.createContext(request), query: createNS, @@ -165,6 +166,7 @@ export class NamespaceController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to create namespace'); } return { @@ -202,6 +204,7 @@ export class NamespaceController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to delete gateway'); } return result.data.forceDeleteNamespace; @@ -249,8 +252,8 @@ const list = gql` `; const item = gql` - query Namespace($gatewayId: String!) { - namespace(gatewayId: $ns) { + query Namespace($ns: String!) { + namespace(ns: $ns) { name displayName scopes { @@ -269,7 +272,7 @@ const item = gql` `; const deleteNS = gql` - mutation ForceDeleteNamespace($gatewayId: String!, $force: Boolean!) { + mutation ForceDeleteNamespace($ns: String!, $force: Boolean!) { forceDeleteNamespace(namespace: $ns, force: $force) } `; diff --git a/src/controllers/v3/IdentifierController.ts b/src/controllers/v3/IdentifierController.ts index b37fc2657..7bed441d4 100644 --- a/src/controllers/v3/IdentifierController.ts +++ b/src/controllers/v3/IdentifierController.ts @@ -4,6 +4,7 @@ import { newProductID, newEnvironmentID, newApplicationID, + newGatewayID, } from '../../services/identifiers'; @Route('identifiers') @@ -11,10 +12,12 @@ import { export class IdentifiersController extends Controller { @Get('{type}') public async getNewID( - @Path() type: 'environment' | 'product' | 'application' + @Path() type: 'environment' | 'product' | 'application' | 'gateway' ): Promise { if (type == 'environment') { return newEnvironmentID(); + } else if (type == 'gateway') { + return newGatewayID(); } else if (type == 'product') { return newProductID(); } else if (type == 'application') { diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index ab7585daf..6f60c5ade 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -11,9 +11,9 @@ import { Body, Get, Tags, + Post, } from 'tsoa'; import { KeystoneService } from '../ioc/keystoneInjector'; -import { assertEqual } from '../ioc/assert'; import { inject, injectable } from 'tsyringe'; import { syncRecords, @@ -22,6 +22,7 @@ import { removeEmpty, removeKeys, transformAllRefID, + syncRecordsThrowErrors, } from '../../batch/feed-worker'; import { GroupAccessService, @@ -39,10 +40,12 @@ import { } from '../../services/org-groups/types'; import { getOrganizations, getOrganizationUnit } from '../../services/keystone'; import { getActivity } from '../../services/keystone/activity'; -import { Activity } from './types'; +import { Activity, Organization } from './types'; import { isParent } from '../../services/org-groups/group-converter-utils'; import { ActivitySummary } from '../../services/keystone/types'; import { ActivityDetail } from './types-extra'; +import { BatchResult } from '../../batch/types'; +import { assertEqual } from '../ioc/assert'; @injectable() @Route('/organizations') @@ -65,6 +68,37 @@ export class OrganizationController extends Controller { })); } + /** + * Create Organization + * > `Required Scope:` GroupAccess.Manage + * + * @summary Create Organizations + * @param ns + * @param body + * @param request + */ + @Put('{org}') + @OperationId('put-organization') + @Security('jwt', ['GroupAccess.Manage']) + public async post( + @Path() org: string, + @Body() body: Organization, + @Request() request: any + ): Promise { + assertEqual( + org == 'ca.bc.gov', + true, + 'org', + 'Only root level is allowed to do this operation' + ); + return await syncRecordsThrowErrors( + this.keystone.createContext(request, true), + 'Organization', + body['name'], + body + ); + } + @Get('{org}') @OperationId('organization-units') public async listOrganizationUnits(@Path() org: string): Promise { diff --git a/src/controllers/v3/ProductController.ts b/src/controllers/v3/ProductController.ts index d78484ee7..81a10de66 100644 --- a/src/controllers/v3/ProductController.ts +++ b/src/controllers/v3/ProductController.ts @@ -25,6 +25,7 @@ import { deleteRecord, getRecord, transformArrayKeyToString, + replaceKey, } from '../../batch/feed-worker'; import { Product } from './types'; import { BatchResult } from '../../batch/types'; @@ -69,7 +70,7 @@ export class ProductController extends Controller { this.keystone.createContext(request), 'Product', body['appId'], - body + replaceKey(body, 'gatewayId', 'namespace') ); } @@ -204,6 +205,7 @@ export class ProductController extends Controller { result.errors.forEach((err: any, ind: number) => { errors[`d${ind}`] = { message: err.message }; }); + logger.error('%j', result); throw new ValidateError(errors, 'Unable to delete product environment'); } return result.data.forceDeleteEnvironment; diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 0210e15a3..56fc2cc74 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -152,16 +152,6 @@ components: type: string type: object additionalProperties: false - Maybe_Scalars-at-String_: - type: string - nullable: true - NamespaceInput: - properties: - displayName: - $ref: '#/components/schemas/Maybe_Scalars-at-String_' - name: - $ref: '#/components/schemas/Maybe_Scalars-at-String_' - type: object ActivityDetail: properties: id: @@ -352,6 +342,54 @@ components: mode: auto environmentDetails: [] owner: janis@gov.bc.ca + OrganizationUnit: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + type: object + additionalProperties: false + Organization: + properties: + extForeignKey: + type: string + name: + type: string + sector: + type: string + title: + type: string + description: + type: string + extSource: + type: string + extRecordHash: + type: string + tags: + items: + type: string + type: array + orgUnits: + items: + $ref: '#/components/schemas/OrganizationUnit' + type: array + type: object + additionalProperties: false GroupPermission: properties: resource: @@ -822,7 +860,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/NamespaceInput' + $ref: '#/components/schemas/Gateway' '/gateways/{gatewayId}': get: operationId: namespace-profile @@ -1056,6 +1094,7 @@ paths: - environment - product - application + - gateway '/gateways/{gatewayId}/issuers': put: operationId: put-issuer @@ -1160,6 +1199,36 @@ paths: security: [] parameters: [] '/organizations/{org}': + put: + operationId: put-organization + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/BatchResult' + description: "Create Organization\n> `Required Scope:` GroupAccess.Manage" + summary: 'Create Organizations' + tags: + - Organizations + security: + - + jwt: + - GroupAccess.Manage + parameters: + - + in: path + name: org + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' get: operationId: organization-units responses: @@ -1557,6 +1626,3 @@ tags: - name: 'Authorization Profiles' description: 'Configure the integration to external Identity Providers' - - - name: Documentation - description: 'View public documentation and publish documentation for your APIs' diff --git a/src/controllers/v3/routes.ts b/src/controllers/v3/routes.ts index c8383682e..1af730ae5 100644 --- a/src/controllers/v3/routes.ts +++ b/src/controllers/v3/routes.ts @@ -115,16 +115,6 @@ 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 - "NamespaceInput": { - "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"displayName":{"ref":"Maybe_Scalars-at-String_"},"name":{"ref":"Maybe_Scalars-at-String_"}},"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": { "dataType": "refObject", "properties": { @@ -235,6 +225,37 @@ 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 + "OrganizationUnit": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + }, + "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 + "Organization": { + "dataType": "refObject", + "properties": { + "extForeignKey": {"dataType":"string"}, + "name": {"dataType":"string"}, + "sector": {"dataType":"string"}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "extSource": {"dataType":"string"}, + "extRecordHash": {"dataType":"string"}, + "tags": {"dataType":"array","array":{"dataType":"string"}}, + "orgUnits": {"dataType":"array","array":{"dataType":"refObject","ref":"OrganizationUnit"}}, + }, + "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 "GroupPermission": { "dataType": "refObject", "properties": { @@ -691,7 +712,7 @@ export function RegisterRoutes(app: express.Router) { async function NamespaceController_create(request: any, response: any, next: any) { const args = { request: {"in":"request","name":"request","required":true,"dataType":"object"}, - vars: {"in":"body","name":"vars","required":true,"ref":"NamespaceInput"}, + vars: {"in":"body","name":"vars","required":true,"ref":"Gateway"}, }; // 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 @@ -903,7 +924,7 @@ export function RegisterRoutes(app: express.Router) { async function IdentifiersController_getNewID(request: any, response: any, next: any) { const args = { - type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]}]}, + type: {"in":"path","name":"type","required":true,"dataType":"union","subSchemas":[{"dataType":"enum","enums":["environment"]},{"dataType":"enum","enums":["product"]},{"dataType":"enum","enums":["application"]},{"dataType":"enum","enums":["gateway"]}]}, }; // 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 @@ -1046,6 +1067,37 @@ export function RegisterRoutes(app: express.Router) { } }); // 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 + app.put('/ds/api/v3/organizations/:org', + authenticateMiddleware([{"jwt":["GroupAccess.Manage"]}]), + + async function OrganizationController_post(request: any, response: any, next: any) { + const args = { + org: {"in":"path","name":"org","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"ref":"Organization"}, + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + + // 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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(OrganizationController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.post.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } catch (err) { + return next(err); + } + }); + // 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 app.get('/ds/api/v3/organizations/:org', async function OrganizationController_listOrganizationUnits(request: any, response: any, next: any) { diff --git a/src/services/identifiers.ts b/src/services/identifiers.ts index 24af1f39b..06e8f7d17 100644 --- a/src/services/identifiers.ts +++ b/src/services/identifiers.ts @@ -23,3 +23,5 @@ export function newEnvironmentID(): string { export function newNamespaceID(): string { return 'gw-' + uuidv4().replace(/-/g, '').toLowerCase().substring(0, 5); } + +export const newGatewayID = newNamespaceID; diff --git a/src/services/keycloak/group-service.ts b/src/services/keycloak/group-service.ts index 3e9127369..45a72b7ca 100644 --- a/src/services/keycloak/group-service.ts +++ b/src/services/keycloak/group-service.ts @@ -163,6 +163,24 @@ export class KeycloakGroupService { return this.kcAdminClient.groups.findOne({ id }); } + public async hasGroup(parentGroupName: string, groupName: string) { + const listOfGroups = this.allGroups + ? this.allGroups + : await this.kcAdminClient.groups.find(); + const groups = listOfGroups.filter( + (group: GroupRepresentation) => group.name == parentGroupName + ); + if ( + groups[0].subGroups.filter( + (group: GroupRepresentation) => group.name == groupName + ).length == 0 + ) { + return false; + } else { + return true; + } + } + public async getGroup(parentGroupName: string, groupName: string) { const listOfGroups = this.allGroups ? this.allGroups diff --git a/src/services/org-groups/namespace.ts b/src/services/org-groups/namespace.ts index 958eb93c2..ef321f51f 100644 --- a/src/services/org-groups/namespace.ts +++ b/src/services/org-groups/namespace.ts @@ -139,8 +139,8 @@ export class NamespaceService { } async checkNamespaceAvailable(ns: string): Promise { - const group = await this.groupService.getGroup('ns', ns); - assert.strictEqual(group === null, true, 'Namespace already exists'); + const groupExists: boolean = await this.groupService.hasGroup('ns', ns); + assert.strictEqual(groupExists, false, 'Namespace already exists'); } async listAssignedNamespacesByOrg(org: string): Promise { diff --git a/src/test/services/batch/batch-utils.test.js b/src/test/services/batch/batch-utils.test.js index 907c6c7d1..7eca7d11c 100644 --- a/src/test/services/batch/batch-utils.test.js +++ b/src/test/services/batch/batch-utils.test.js @@ -5,6 +5,7 @@ import { removeEmpty, removeKeys, dot, + replaceKey, } from '../../../batch/feed-worker'; import YAML from 'js-yaml'; @@ -137,4 +138,34 @@ describe('Batch Utilities', function () { expect(dot(value, '.nowhere')).toBe(null); expect(dot(value, 'a.b.c')).toBe(null); }); + + it('should replace key', async function () { + const input = { + gatewayId: 'gw-1234', + name: 'sample name', + attribute: null, + block: { + type: 'bland', + }, + nested: { + id: '000011', + another: null, + }, + }; + const output = { + name: 'sample name', + attribute: null, + block: { + type: 'bland', + }, + nested: { + id: '000011', + another: null, + }, + namespace: 'gw-1234', + }; + + const result = replaceKey(input, 'gatewayId', 'namespace'); + expect(JSON.stringify(result)).toBe(JSON.stringify(output)); + }); }); diff --git a/src/tsoa-v3.json b/src/tsoa-v3.json index 10e323533..06d33b2f6 100644 --- a/src/tsoa-v3.json +++ b/src/tsoa-v3.json @@ -56,10 +56,6 @@ { "name": "Authorization Profiles", "description": "Configure the integration to external Identity Providers" - }, - { - "name": "Documentation", - "description": "View public documentation and publish documentation for your APIs" } ] },