From 36294945fc70af90146216c085a0bc880c0f6643 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 15 Nov 2021 03:25:20 -0800 Subject: [PATCH 1/2] merge fix --- .gitignore | 2 + src/.storybook/main.js | 1 + src/auth/auth-oauth2-proxy.js | 51 +- src/auth/auth-tsoa.ts | 2 +- src/auth/scope-role-utils.ts | 20 +- src/authz/enforcement.ts | 1 + .../httplocalhost3000admingraphiql-19e539.gql | 18 + .../httplocalhost3000admingraphiql-70d2f3.gql | 16 + .../httplocalhost3000admingraphiql-a7b31f.gql | 15 + ...tplocalhost4180managerconsumers-602067.gql | 14 + ...tplocalhost4180managerconsumers-7b48ba.gql | 29 + ...ttplocalhost4180managerrequests-1cfabf.gql | 72 ++ ...ttplocalhost4180managerrequests-d90843.gql | 56 ++ ...ttplocalhost4180managerrequests-f1a699.gql | 55 ++ src/authz/matrix.csv | 12 + src/batch/data-rules.ts | 3 +- src/batch/feed-worker.ts | 26 +- .../transformations/connectExclusiveList.ts | 18 +- .../transformations/connectExclusiveOne.ts | 3 + src/batch/transformations/mapNamespace.ts | 34 +- src/controllers/ContentController.ts | 4 +- src/controllers/DirectoryController.ts | 90 ++- src/controllers/NamespaceController.ts | 53 +- src/controllers/ioc/keystoneInjector.ts | 10 +- src/controllers/openapi.yaml | 17 +- src/controllers/routes.ts | 32 +- src/lists/Activity.js | 94 +-- src/lists/Content.js | 2 + src/lists/Environment.js | 1 + src/lists/extensions/AliasedQueries.ts | 3 +- src/lists/extensions/Common.ts | 4 +- src/nextapp/.eslintrc.js | 3 +- .../components/access-requests/viewer.tsx | 1 + .../actions-menu/actions-menu.stories.mdx | 37 + .../components/actions-menu/actions-menu.tsx | 32 + src/nextapp/components/actions-menu/index.ts | 1 + .../api-product-item/api-product-item.tsx | 104 +++ .../components/api-product-item/index.ts | 2 + .../application-services.tsx | 51 ++ .../components/application-services/index.ts | 1 + .../components/auth-action/auth-action.tsx | 45 +- src/nextapp/components/card/card.stories.mdx | 136 ++++ src/nextapp/components/card/card.stories.tsx | 14 - src/nextapp/components/card/card.tsx | 49 +- .../confirmation-dialog.tsx | 79 ++ .../components/confirmation-dialog/index.ts | 1 + .../controls-list/controls-list.tsx | 14 +- .../controls-list/delete-control.tsx | 6 +- .../discovery-list/discovery-list-item.tsx | 42 +- .../discovery-list/discovery-list.tsx | 4 +- .../components/empty-pane/empty-pane.tsx | 8 +- .../environment-config/environment-config.tsx | 1 + .../environment-plugins.tsx | 2 + .../templates/kong-api-key-only.ts | 13 + .../environments-list/edit-environment.tsx | 1 + src/nextapp/components/header/header.tsx | 19 +- .../namespace-manager/namespace-manager.tsx | 11 +- .../namespace-menu/namespace-menu.tsx | 11 +- .../new-application-dialog.tsx | 16 +- .../components/page-header/page-header.tsx | 37 +- src/nextapp/components/table/index.ts | 1 + .../components/table/table.stories.mdx | 147 ++++ src/nextapp/components/table/table.tsx | 126 ++++ src/nextapp/pages/_app.tsx | 77 +- .../pages/devportal/api-directory/[id].tsx | 228 +++--- .../pages/devportal/api-directory/index.tsx | 6 +- .../pages/devportal/applications/index.tsx | 221 ++++-- .../pages/devportal/requests/new/[id].tsx | 2 +- src/nextapp/pages/manager/consumers/[id].tsx | 1 + src/nextapp/pages/manager/consumers/index.tsx | 7 +- .../pages/manager/namespaces/index.tsx | 283 ++++--- src/nextapp/pages/manager/requests/[id].tsx | 38 +- src/nextapp/pages/signout.tsx | 26 +- .../nextapp/public/images/favicon.png | Bin .../shared/services/auth/auth-context.tsx | 17 +- .../shared/services/auth/use-session.ts | 3 + src/nextapp/shared/services/utils.ts | 2 + src/nextapp/shared/theme.ts | 78 +- src/nextapp/shared/types/query.types.ts | 210 ++++++ src/package-lock.json | 704 +++++++++++++++++- src/package.json | 3 + src/services/identifiers.ts | 26 +- src/services/issuerMisconfigError.ts | 20 +- src/services/keycloak/client-service.ts | 18 +- src/services/keycloak/group-service.ts | 22 +- src/services/keycloak/index.ts | 5 +- src/services/keycloak/user-service.ts | 9 + src/services/keystone/access-request.ts | 215 ++++-- src/services/keystone/batch-service.ts | 2 +- src/services/keystone/gateway-service.ts | 45 ++ src/services/keystone/index.ts | 7 + src/services/keystone/metrics.ts | 157 ++++ src/services/keystone/product-environment.ts | 61 +- src/services/keystone/service-access.ts | 78 +- src/services/keystone/types.ts | 210 ++++++ src/services/kong/acl.ts | 47 ++ src/services/kong/consumer-service.ts | 2 +- src/services/kong/consumer.ts | 47 ++ src/services/kong/index.ts | 6 + src/services/report/data/consumer-access.ts | 431 +++++++++++ src/services/report/data/consumer-controls.ts | 77 ++ src/services/report/data/consumer-metrics.ts | 71 ++ src/services/report/data/consumer-requests.ts | 71 ++ src/services/report/data/gateway-controls.ts | 70 ++ src/services/report/data/gateway-metrics.ts | 50 ++ src/services/report/data/index.ts | 9 + src/services/report/data/namespaces.ts | 46 ++ src/services/report/data/ns-access.ts | 53 ++ src/services/report/data/service-access.ts | 294 ++++++++ src/services/report/output/structure.ts | 250 +++++++ src/services/report/output/xls-generator.ts | 39 + src/services/report/workbook.service.ts | 90 +++ src/services/uma2/policy-service.ts | 4 +- src/services/uma2/token-service.ts | 6 +- src/services/utils.ts | 16 + src/services/workflow/apply.ts | 2 + src/services/workflow/generate-credential.ts | 6 +- src/services/workflow/get-namespaces.ts | 232 ++++++ src/services/workflow/index.ts | 6 + .../workflow/link-consumer-to-namespace.ts | 2 +- .../workflow/validate-active-environment.ts | 18 + src/test/mock-server/data/bceid_sample.js | 9 + src/test/mock-server/server.js | 8 +- src/test/services/keycloak.test.js | 71 -- .../keycloak/client-registration.test.ts | 74 ++ .../services/keycloak/client-service.test.ts | 197 +++++ src/test/services/keycloak/keycloak.test.ts | 90 +++ src/test/services/keycloak/mock-queries.ts | 10 + src/test/services/keystone.test.js | 110 --- .../keystone/gateway-consumer.test.ts | 69 ++ src/test/services/keystone/keystone.test.js | 162 ++++ src/test/services/keystone/metrics.test.ts | 86 +++ .../keystone/product-environment.test.ts | 103 +++ .../services/keystone/service-access.test.ts | 38 + .../services/keystone/user-service.test.ts | 38 + .../{kong.test.js => kong.test.js.excl} | 0 src/test/services/kong/acl.test.ts.excl | 1 + src/test/services/reports/nsList.test.js | 162 ++++ .../{tasked.test.js => tasked.test.js.excl} | 0 src/test/services/uma2/policy.test.ts | 103 +++ src/test/services/uma2/resource.test.ts | 127 ++++ ...test.js => client-credential.test.js.excl} | 0 .../services/workflow/get-namespaces.test.ts | 260 +++++++ ....test.js => kong-api-key-acl.test.js.excl} | 0 ... validate-active-environment.test.js.excl} | 0 src/test/servicests/uma2-policy.test.ts | 35 - src/yarn.lock | 278 ++++++- 147 files changed, 7564 insertions(+), 966 deletions(-) create mode 100644 src/authz/graphql-whitelist/httplocalhost3000admingraphiql-19e539.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost3000admingraphiql-70d2f3.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost3000admingraphiql-a7b31f.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managerconsumers-602067.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managerconsumers-7b48ba.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managerrequests-1cfabf.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managerrequests-d90843.gql create mode 100644 src/authz/graphql-whitelist/httplocalhost4180managerrequests-f1a699.gql create mode 100644 src/nextapp/components/actions-menu/actions-menu.stories.mdx create mode 100644 src/nextapp/components/actions-menu/actions-menu.tsx create mode 100644 src/nextapp/components/actions-menu/index.ts create mode 100644 src/nextapp/components/api-product-item/api-product-item.tsx create mode 100644 src/nextapp/components/api-product-item/index.ts create mode 100644 src/nextapp/components/application-services/application-services.tsx create mode 100644 src/nextapp/components/application-services/index.ts create mode 100644 src/nextapp/components/card/card.stories.mdx delete mode 100644 src/nextapp/components/card/card.stories.tsx create mode 100644 src/nextapp/components/confirmation-dialog/confirmation-dialog.tsx create mode 100644 src/nextapp/components/confirmation-dialog/index.ts create mode 100644 src/nextapp/components/environment-plugins/templates/kong-api-key-only.ts create mode 100644 src/nextapp/components/table/index.ts create mode 100644 src/nextapp/components/table/table.stories.mdx create mode 100644 src/nextapp/components/table/table.tsx rename "src/nextapp/public/(PNG Image, 62\302\240\303\227\302\24055 pixels)" => src/nextapp/public/images/favicon.png (100%) create mode 100644 src/services/keystone/metrics.ts create mode 100644 src/services/kong/acl.ts create mode 100644 src/services/kong/consumer.ts create mode 100644 src/services/report/data/consumer-access.ts create mode 100644 src/services/report/data/consumer-controls.ts create mode 100644 src/services/report/data/consumer-metrics.ts create mode 100644 src/services/report/data/consumer-requests.ts create mode 100644 src/services/report/data/gateway-controls.ts create mode 100644 src/services/report/data/gateway-metrics.ts create mode 100644 src/services/report/data/index.ts create mode 100644 src/services/report/data/namespaces.ts create mode 100644 src/services/report/data/ns-access.ts create mode 100644 src/services/report/data/service-access.ts create mode 100644 src/services/report/output/structure.ts create mode 100644 src/services/report/output/xls-generator.ts create mode 100644 src/services/report/workbook.service.ts create mode 100644 src/services/utils.ts create mode 100644 src/services/workflow/get-namespaces.ts create mode 100644 src/test/mock-server/data/bceid_sample.js delete mode 100644 src/test/services/keycloak.test.js create mode 100644 src/test/services/keycloak/client-registration.test.ts create mode 100644 src/test/services/keycloak/client-service.test.ts create mode 100644 src/test/services/keycloak/keycloak.test.ts create mode 100644 src/test/services/keycloak/mock-queries.ts delete mode 100644 src/test/services/keystone.test.js create mode 100644 src/test/services/keystone/gateway-consumer.test.ts create mode 100644 src/test/services/keystone/keystone.test.js create mode 100644 src/test/services/keystone/metrics.test.ts create mode 100644 src/test/services/keystone/product-environment.test.ts create mode 100644 src/test/services/keystone/service-access.test.ts create mode 100644 src/test/services/keystone/user-service.test.ts rename src/test/services/{kong.test.js => kong.test.js.excl} (100%) create mode 100644 src/test/services/kong/acl.test.ts.excl create mode 100644 src/test/services/reports/nsList.test.js rename src/test/services/{tasked.test.js => tasked.test.js.excl} (100%) create mode 100644 src/test/services/uma2/policy.test.ts create mode 100644 src/test/services/uma2/resource.test.ts rename src/test/services/workflow/{client-credential.test.js => client-credential.test.js.excl} (100%) create mode 100644 src/test/services/workflow/get-namespaces.test.ts rename src/test/services/workflow/{kong-api-key-acl.test.js => kong-api-key-acl.test.js.excl} (100%) rename src/test/services/workflow/{validate-active-environment.test.js => validate-active-environment.test.js.excl} (100%) delete mode 100644 src/test/servicests/uma2-policy.test.ts diff --git a/.gitignore b/.gitignore index bb497f10f..9976cff5b 100644 --- a/.gitignore +++ b/.gitignore @@ -112,5 +112,7 @@ _tmp kc.js +.config + # vs code settings .vscode \ No newline at end of file diff --git a/src/.storybook/main.js b/src/.storybook/main.js index e6f71f236..7fdfc248c 100644 --- a/src/.storybook/main.js +++ b/src/.storybook/main.js @@ -17,6 +17,7 @@ module.exports = { '../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)', '../nextapp/components/**/*.stories.@(js|jsx|ts|tsx|mdx)', + '../nextapp/components/**/*.stories.mdx', ], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], webpackFinal: async (config) => { diff --git a/src/auth/auth-oauth2-proxy.js b/src/auth/auth-oauth2-proxy.js index b8cdf0984..b7c0ed2d5 100644 --- a/src/auth/auth-oauth2-proxy.js +++ b/src/auth/auth-oauth2-proxy.js @@ -11,7 +11,7 @@ const querystring = require('querystring'); const jwt = require('express-jwt'); const jwksRsa = require('jwks-rsa'); const jwtDecoder = require('jwt-decode'); -const { deriveRoleFromUsername } = require('./scope-role-utils'); +const { deriveRoleFromUsername, scopesToRoles } = require('./scope-role-utils'); const proxy = process.env.EXTERNAL_URL; const authLogoutUrl = @@ -155,6 +155,33 @@ class Oauth2ProxyAuthStrategy { } ); + app.put( + '/admin/switch', + [verifyJWT, checkExpired], + async (req, res, next) => { + // Switch to "no namespace" - aka "clear namespace" + try { + const jti = req['oauth_user']['jti']; // JWT ID - Unique Identifier for the token + const username = req['oauth_user']['preferred_username']; // Username included in token + // The oauth2_proxy is handling the refresh token; so there can be a new jti + logger.info( + '[ns-clear] %s -> %s : %s', + req.user.jti, + jti, + req.user.jti === jti ? 'SAME TOKEN' : 'REFRESHED TOKEN!' + ); + await this.assign_namespace(req.user.jti, jti, username, { + rsname: null, + scopes: [], + }); + res.json({ switch: true }); + } catch (err) { + logger.error('Error clearing namespace %s', err); + res.status(400).json({ switch: false, error: 'ns_cleared_fail' }); + } + } + ); + app.put( '/admin/switch/:ns', [verifyJWT, checkExpired], @@ -205,27 +232,13 @@ class Oauth2ProxyAuthStrategy { async assign_namespace(jti, newJti, username, umaAuthDetails) { const namespace = umaAuthDetails['rsname']; const scopes = umaAuthDetails['scopes']; - const _roles = []; - if (scopes.includes('Namespace.Manage')) { - _roles.push('api-owner'); - } - if (scopes.includes('Namespace.View')) { - _roles.push('provider-user'); - } - if (scopes.includes('CredentialIssuer.Admin')) { - _roles.push('credential-admin'); - } - if (scopes.includes('Access.Manage')) { - _roles.push('access-manager'); - } - - _roles.push('portal-user'); - _roles.push(deriveRoleFromUsername(username)); + const _roles = scopesToRoles(username, scopes); const roles = JSON.stringify(_roles); - const users = this.keystone.getListByKey(this.listKey); - let results = await users.adapter.find({ jti: jti }); + // should be TemporaryIdentity + const idList = this.keystone.getListByKey(this.listKey); + let results = await idList.adapter.find({ jti: jti }); let tempId = results[0]['id']; const { errors } = await this.keystone.executeGraphQL({ diff --git a/src/auth/auth-tsoa.ts b/src/auth/auth-tsoa.ts index 257783a67..06ca776fa 100644 --- a/src/auth/auth-tsoa.ts +++ b/src/auth/auth-tsoa.ts @@ -68,7 +68,7 @@ export function expressAuthentication( if (authzerr) { reject(new Error('Access Denied')); } else { - resolve(request.oauth_user); + resolve({ ...request.oauth_user, ...{ scope: scopes[0] } }); } } ); diff --git a/src/auth/scope-role-utils.ts b/src/auth/scope-role-utils.ts index b708769c2..fecf2839d 100644 --- a/src/auth/scope-role-utils.ts +++ b/src/auth/scope-role-utils.ts @@ -2,18 +2,26 @@ export function scopes(scopeString: string) { return scopeString.split(' '); } -export function scopesToRoles(scopes: string[]) { +export function scopesToRoles(username: string, scopes: string[]): string[] { const _roles = []; if (scopes.includes('Namespace.Manage')) { _roles.push('api-owner'); - } else { - // For now, make everyone an api-owner if they have access to a namespace - _roles.push('api-owner'); } - if (scopes.includes('Namespace.Manage')) { + if (scopes.includes('Namespace.View')) { + _roles.push('provider-user'); + } + if (scopes.includes('CredentialIssuer.Admin')) { _roles.push('credential-admin'); } - return JSON.stringify(_roles); + if (scopes.includes('Access.Manage')) { + _roles.push('access-manager'); + } + + _roles.push('portal-user'); + if (username != null) { + _roles.push(deriveRoleFromUsername(username)); + } + return _roles; } export function deriveRoleFromUsername(username: string) { diff --git a/src/authz/enforcement.ts b/src/authz/enforcement.ts index 6de67ca50..ab0ac0f22 100644 --- a/src/authz/enforcement.ts +++ b/src/authz/enforcement.ts @@ -133,6 +133,7 @@ export function EnforcementPoint(params: any) { tid: item == null ? null : item.id, id: item == null ? null : item.userId, roles: roles, + scopes: item == null ? null : item.scopes, namespace: item == null ? null : item.namespace, item: originalInput, }, diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-19e539.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-19e539.gql new file mode 100644 index 000000000..b569691ff --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-19e539.gql @@ -0,0 +1,18 @@ + + query GET_APPLICATION_SERVICES($appId: String!) { + myServiceAccesses(where: { application: { appId: $appId } }) { + id + name + active + application { + name + } + productEnvironment { + id + name + product { + name + } + } + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-70d2f3.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-70d2f3.gql new file mode 100644 index 000000000..30e644f80 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-70d2f3.gql @@ -0,0 +1,16 @@ + + query MyApplications { + myApplications { + id + appId + description + name + owner { + name + } + } + allTemporaryIdentities { + id + userId + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-a7b31f.gql b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-a7b31f.gql new file mode 100644 index 000000000..fe88b1910 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost3000admingraphiql-a7b31f.gql @@ -0,0 +1,15 @@ +{ + myApplications { + id + appId + description + name + owner { + name + } + } + allTemporaryIdentities { + id + userId + } +} diff --git a/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-602067.gql b/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-602067.gql new file mode 100644 index 000000000..7f3dd7f68 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-602067.gql @@ -0,0 +1,14 @@ + + mutation ToggleConsumerRoles( + $prodEnvId: ID! + $consumerUsername: String! + $roleName: String! + $grant: Boolean! + ) { + updateConsumerRoleAssignment( + prodEnvId: $prodEnvId + consumerUsername: $consumerUsername + roleName: $roleName + grant: $grant + ) + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-7b48ba.gql b/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-7b48ba.gql new file mode 100644 index 000000000..a520b438e --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managerconsumers-7b48ba.gql @@ -0,0 +1,29 @@ + + query GetConsumers { + allServiceAccessesByNamespace( + first: 200 + orderBy: "updatedAt_DESC" + where: { consumer: { username_not_starts_with: "sa-" } } + ) { + namespace + consumer { + id + username + aclGroups + customId + plugins { + name + } + tags + updatedAt + } + application { + name + appId + } + } + + allAccessRequestsByNamespace(where: { isComplete_not: true }) { + id + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managerrequests-1cfabf.gql b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-1cfabf.gql new file mode 100644 index 000000000..2666fff2a --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-1cfabf.gql @@ -0,0 +1,72 @@ + + query GetAccessRequest($id: ID!, $rid: String!) { + AccessRequest(where: { id: $id }) { + id + name + isApproved + isIssued + controls + additionalDetails + createdAt + requestor { + name + username + email + } + application { + name + } + serviceAccess { + id + consumer { + id + username + plugins { + id + name + extForeignKey + config + service { + id + name + extForeignKey + } + route { + id + name + extForeignKey + } + } + } + } + productEnvironment { + name + additionalDetailsToRequest + product { + name + } + credentialIssuer { + availableScopes + clientRoles + } + } + } + + allActivities(sortBy: createdAt_DESC, where: { refId: $rid }) { + id + type + name + action + result + message + context + refId + namespace + extRefId + createdAt + actor { + name + username + } + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managerrequests-d90843.gql b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-d90843.gql new file mode 100644 index 000000000..1ddafcbcc --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-d90843.gql @@ -0,0 +1,56 @@ + + query GetAccessRequest($id: ID!, $rid: String!) { + AccessRequest(where: { id: $id }) { + id + name + isApproved + isIssued + controls + additionalDetails + createdAt + requestor { + name + username + email + } + application { + name + } + serviceAccess { + id + consumer { + id + username + } + } + productEnvironment { + name + additionalDetailsToRequest + product { + name + } + credentialIssuer { + availableScopes + clientRoles + } + } + } + + allActivities(sortBy: createdAt_DESC, where: { refId: $rid }) { + id + type + name + action + result + message + context + refId + namespace + extRefId + createdAt + actor { + name + username + } + } + } diff --git a/src/authz/graphql-whitelist/httplocalhost4180managerrequests-f1a699.gql b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-f1a699.gql new file mode 100644 index 000000000..15bfebfe1 --- /dev/null +++ b/src/authz/graphql-whitelist/httplocalhost4180managerrequests-f1a699.gql @@ -0,0 +1,55 @@ + + query GetAccessRequest($id: ID!, $rid: String!) { + AccessRequest(where: { id: $id }) { + id + name + isApproved + isIssued + controls + additionalDetails + createdAt + requestor { + name + username + email + } + application { + name + } + serviceAccess { + id + consumer { + username + } + } + productEnvironment { + name + additionalDetailsToRequest + product { + name + } + credentialIssuer { + availableScopes + clientRoles + } + } + } + + allActivities(sortBy: createdAt_DESC, where: { refId: $rid }) { + id + type + name + action + result + message + context + refId + namespace + extRefId + createdAt + actor { + name + username + } + } + } diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index ae8531ac7..f2ba68fa8 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -6,6 +6,8 @@ ACCESS MANAGER,,,AccessRequest,read,,,,,access-manager,,,allow, ACCESS MANAGER,,,AccessRequest,,"update,delete",,,,access-manager,,,allow, ACCESS MANAGER,,allAccessRequestsByNamespace,,,,,,,access-manager,,,allow,filterByEnvironmentPackageNS ACCESS MANAGER,,allServiceAccessesByNamespace,,,,,,,access-manager,,,allow,filterByEnvironmentProductNSOrNS +ACCESS MANAGER,,allProductsByNamespace,,,,,,,access-manager,,,allow,filterByUserNS +ACCESS MANAGER,,allGatewayServicesByNamespace,,,,,,,access-manager,,,allow,filterByUserNS ACCESS MANAGER,,getGatewayConsumerPlugins,,,,,,,access-manager,,,allow, ACCESS MANAGER,,deleteGatewayConsumerPlugin,,,,,,,access-manager,,,allow, ACCESS MANAGER,,updateGatewayConsumerPlugin,,,,,,,access-manager,,,allow, @@ -25,6 +27,8 @@ ACCESS MANAGER,,BusinessProfile,,,,,,,access-manager,,,allow, CREDENTIAL ADMIN,,,CredentialIssuer,,"update,delete",,,,credential-admin,,,allow,filterByOwner CREDENTIAL ADMIN,,,CredentialIssuer,create,,,,,credential-admin,,,allow, CREDENTIAL ADMIN,,,CredentialIssuer,read,,,,,credential-admin,,,allow, +CREDENTIAL ADMIN,,,CredentialIssuer,update,,namespace,,,credential-admin,,,deny, +CREDENTIAL ADMIN,,,CredentialIssuer,,"create,read",namespace,,,credential-admin,,,allow, CREDENTIAL ADMIN,,OwnedCredentialIssuer,,,,,,,credential-admin,,,allow, CREDENTIAL ADMIN,,allCredentialIssuersByNamespace,,,,,,,credential-admin,,,allow,filterByUserNS CREDENTIAL ADMIN,,,User,read,,,,,credential-admin,,,allow, @@ -113,6 +117,8 @@ Developer Role Rules,,,OrganizationUnit,read,,,,,portal-user,,,allow, Developer Role Rules,,,GatewayService,read,,,,,portal-user,,,allow, Developer Role Rules,,,GatewayRoute,read,,,,,portal-user,,,allow, Developer Role Rules,,,GatewayConsumer,read,,,,,portal-user,,,allow, +Developer Role Rules,,,GatewayPlugin,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Metric,read,,,,,portal-user,,,allow, Developer Role Rules,,,Legal,read,,,,,portal-user,,,allow, Developer Role Rules,,,TemporaryIdentity,read,,,,,portal-user,,,allow,filterByTemporaryIdentity Developer Role Rules,,,User,read,,,,,portal-user,,,allow,filterBySelf @@ -142,6 +148,9 @@ Developer Role Rules - Fields,,,AccessRequest,create,,"name,controls,requestor,a Developer Role Rules - Fields,,,AccessRequest,,"read,update",credential,,,portal-user,,,allow, Developer Role Rules - Fields,,,AccessRequest,read,,"name,controls,application,productEnvironment,isIssued",,,portal-user,,,allow, Developer Role Rules - Fields,,,ServiceAccess,read,,"consumer,application",,,portal-user,,,allow, +Content Publisher,,,Content,,create,,,,,Content.Publish,,allow, +Content Publisher,,,Content,,update,,,,,Content.Publish,,allow,filterByUserNS +Content Publisher,,,Content,,update,namespace,,,,,,deny, API Owner Role - All Fields,,,,,read,,*,,,,"api-owner,provider-user",allow, API Owner Role - All Fields,,,,,"update,create",,*,,api-owner,,,allow, Portal User,,DiscoverableProduct,,,,,,,portal-user,,,allow, @@ -159,7 +168,10 @@ Portal User or Guest,,allProducts,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,environments,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,allDatasets,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,Environment,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,allLegals,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,allCredentialIssuers,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,,,"portal-user,guest",allow, +Portal User or Guest,,,Legal,read,,"title,description",,,,,"portal-user,guest",allow, Portal User or Guest,,allContents,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,DiscoverableProduct,,,,,,,,,"portal-user,guest",allow, Portal User or Guest,,services,,,,,,,,,"portal-user,guest",allow, diff --git a/src/batch/data-rules.ts b/src/batch/data-rules.ts index 1320f0b6e..dea10521b 100644 --- a/src/batch/data-rules.ts +++ b/src/batch/data-rules.ts @@ -396,13 +396,14 @@ export const metadata = { 'order', 'isPublic', 'isComplete', + 'namespace', 'tags', 'publishDate', 'slug', ], transformations: { tags: { name: 'toStringDefaultArray' }, - namespace: { name: 'mapNamespace' }, + namespace: { name: 'mapNamespace', update: false }, }, }, ContentBySlug: { diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index 62bae4334..06d0c143d 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -59,7 +59,7 @@ const transformations = { // return result // } -export const putFeedWorker = async (keystone: any, req: any, res: any) => { +export const putFeedWorker = async (context: any, req: any, res: any) => { const entity = req.params['entity']; const eid = 'id' in req.params ? req.params['id'] : req.body['id']; const json = req.body; @@ -80,16 +80,16 @@ export const putFeedWorker = async (keystone: any, req: any, res: any) => { JSON.stringify(req.body) ); - const context = keystone.createContext({ skipAccessControl: true }); + //const context = keystone.createContext({ skipAccessControl: true }); const result = await syncRecords(context, entity, eid, json); res.status(result.status).json(result); }; -export const deleteFeedWorker = async (keystone: any, req: any, res: any) => { +export const deleteFeedWorker = async (context: any, req: any, res: any) => { const feedEntity = req.params['entity']; const eid = req.params['id']; const json = req.body; - const context = keystone.createContext({ skipAccessControl: true }); + //const context = keystone.createContext({ skipAccessControl: true }); const batchService = new BatchService(context); assert.strictEqual(feedEntity in metadata, true); @@ -116,12 +116,12 @@ export const deleteFeedWorker = async (keystone: any, req: any, res: any) => { } }; -export const getFeedWorker = async (keystone: any, req: any, res: any) => { +export const getFeedWorker = async (context: any, req: any, res: any) => { const feedEntity = req.params['entity']; const refKey = req.params['refKey']; const refKeyValue = req.params['refKeyValue']; - const context = keystone.createContext({ skipAccessControl: true }); + //const context = keystone.createContext({ skipAccessControl: true }); const batchService = new BatchService(context); assert.strictEqual(feedEntity in metadata, true); @@ -190,7 +190,7 @@ function buildQueryResponse(md: any): string[] { } export const syncRecords = async function ( - keystone: any, + context: any, feedEntity: string, eid: string, json: any, @@ -205,7 +205,7 @@ export const syncRecords = async function ( 'This entity is only part of a child.' ); - const batchService = new BatchService(keystone); + const batchService = new BatchService(context); // pre-lookup hook that can be used to handle special cases, // such as for Kong, cleaning up records where the service or route has been renamed @@ -217,7 +217,7 @@ export const syncRecords = async function ( true, `Hook ${hook} missing!` ); - await hooks['pre-lookup'][hook](keystone, entity, md, eid, json); + await hooks['pre-lookup'][hook](context, entity, md, eid, json); } } @@ -241,7 +241,7 @@ export const syncRecords = async function ( if (transformInfo.syncFirst) { // handle these children independently first - return a list of IDs const allIds = await syncListOfRecords( - keystone, + context, transformInfo.list, json[transformKey] ); @@ -254,7 +254,7 @@ export const syncRecords = async function ( json[transformKey + '_ids'] = allIds.map((status) => status.id); } const transformMutation = await transformations[transformInfo.name]( - keystone, + context, transformInfo, null, json, @@ -310,7 +310,7 @@ export const syncRecords = async function ( if (transformInfo.syncFirst) { // handle these children independently first - return a list of IDs const allIds = await syncListOfRecords( - keystone, + context, transformInfo.list, json[transformKey] ); @@ -324,7 +324,7 @@ export const syncRecords = async function ( } const transformMutation = await transformations[transformInfo.name]( - keystone, + context, transformInfo, localRecord, json, diff --git a/src/batch/transformations/connectExclusiveList.ts b/src/batch/transformations/connectExclusiveList.ts index 0a15fb602..cb5d9e2b6 100644 --- a/src/batch/transformations/connectExclusiveList.ts +++ b/src/batch/transformations/connectExclusiveList.ts @@ -1,4 +1,9 @@ -export function connectExclusiveList( +import { BatchService } from '../../services/keystone/batch-service'; +import { Logger } from '../../logger'; + +const logger = Logger('batch.connectExclusiveList'); + +export async function connectExclusiveList( keystone: any, transformInfo: any, currentData: any, @@ -16,6 +21,17 @@ export function connectExclusiveList( ) { return null; } + // Because this is an exclusive list, delete records that are no longer relevant + if (currentData != null) { + const deleted = currentData[fieldKey] + .map((d: any) => d.id) + .filter((n: string) => !inputData[fieldKey + '_ids'].includes(n)); + logger.debug('Deletions? %j', deleted); + if (deleted.length > 0) { + const batchService = new BatchService(keystone); + await batchService.removeAll(transformInfo.list, deleted); + } + } return { disconnectAll: true, connect: inputData[fieldKey + '_ids'].map((id: string) => { diff --git a/src/batch/transformations/connectExclusiveOne.ts b/src/batch/transformations/connectExclusiveOne.ts index 8f6389359..23fb6ac00 100644 --- a/src/batch/transformations/connectExclusiveOne.ts +++ b/src/batch/transformations/connectExclusiveOne.ts @@ -5,6 +5,9 @@ export function connectExclusiveOne( inputData: any, fieldKey: string ) { + if (inputData[fieldKey + '_ids'].length == 0) { + return null; + } if ( currentData != null && currentData[fieldKey] && diff --git a/src/batch/transformations/mapNamespace.ts b/src/batch/transformations/mapNamespace.ts index 8ed987460..466fb5c81 100644 --- a/src/batch/transformations/mapNamespace.ts +++ b/src/batch/transformations/mapNamespace.ts @@ -1,11 +1,25 @@ - -export function mapNamespace (keystone: any, transformInfo: any, currentData: any, inputData: any, key: string) { - if (inputData['tags'] != null) { - const val = inputData['tags'] - .filter((tag:string) => tag.startsWith('ns.') && tag.indexOf('.', 3) == -1) - .map((tag:string) => tag.substring(3)) [0] - return currentData != null && currentData[key] === val ? null : val - } else { - return null - } +export function mapNamespace( + keystone: any, + transformInfo: any, + currentData: any, + inputData: any, + key: string +) { + if (inputData['tags'] != null) { + const val = inputData['tags'] + .filter( + (tag: string) => tag.startsWith('ns.') && tag.indexOf('.', 3) == -1 + ) + .map((tag: string) => tag.substring(3))[0]; + // if ( + // currentData != null && + // currentData[key] != val && + // transformInfo.update === false + // ) { + // throw Error('Namespace can not be updated.'); + // } + return currentData != null && currentData[key] === val ? null : val; + } else { + return null; + } } diff --git a/src/controllers/ContentController.ts b/src/controllers/ContentController.ts index b9bdb567d..dc39c1f9d 100644 --- a/src/controllers/ContentController.ts +++ b/src/controllers/ContentController.ts @@ -15,7 +15,7 @@ import express from 'express'; import multer from 'multer'; import { DateTime, Markdown } from '@keystonejs/fields'; -interface Content { +interface ContentSummary { kind?: string; externalLink: string; title?: string; @@ -42,7 +42,7 @@ export class ContentController extends Controller { @OperationId('put-content') public async putContent( @Path() ns: string, - @Body() body: Content, + @Body() body: ContentSummary, @Request() request: any ): Promise { return await syncRecords( diff --git a/src/controllers/DirectoryController.ts b/src/controllers/DirectoryController.ts index fe706ff5a..76dac0175 100644 --- a/src/controllers/DirectoryController.ts +++ b/src/controllers/DirectoryController.ts @@ -1,18 +1,8 @@ -import { - Controller, - OperationId, - Request, - Get, - Path, - Route, - Security, -} from 'tsoa'; +import { Controller, OperationId, Get, Path, Route } from 'tsoa'; import { KeystoneService } from './ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; -import { syncRecords } from '../batch/feed-worker'; import { gql } from 'graphql-request'; import { Product } from '@/services/keystone/types'; -import { strict as assert } from 'assert'; @injectable() @Route('/directory') export class DirectoryController extends Controller { @@ -29,24 +19,63 @@ export class DirectoryController extends Controller { context: this.keystone.sudo(), query: list, }); - return result.data.allDiscoverableProducts; + return transform(result.data.allDiscoverableProducts); } @Get('{id}') @OperationId('directory-item') public async get(@Path() id: string): Promise { - const product: Product = ( - await this.keystone.executeGraphQL({ - context: this.keystone.sudo(), - query: item, - variables: { id }, - }) - ).data.DiscoverableProduct; - assert.strictEqual(product != null, true, `Product Not Found`); - return product; + const result = await this.keystone.executeGraphQL({ + context: this.keystone.sudo(), + query: item, + variables: { id }, + }); + return transform( + transformSetAnonymous(result.data.allDiscoverableProducts) + )[0]; } } +function transformSetAnonymous(products: Product[]) { + products.forEach((prod) => { + prod.environments.forEach((env) => { + env.services.forEach((svc) => { + svc.plugins && + svc.plugins + .filter( + (plugin) => + plugin.name == 'key-auth' || plugin.name == 'jwt-keycloak' + ) + .forEach((plugin) => { + const config = JSON.parse(plugin.config); + if (config.anonymous) { + (env as any).anonymous = true; + } + }); + }); + }); + }); + return products; +} +function transform(products: Product[]) { + return products.reduce((accumulator: any, prod: any) => { + if (prod.dataset === null) { + // drop it + } else { + const dataset = accumulator.filter( + (a: any) => a.name === prod.dataset?.name + ); + if (dataset.length == 0) { + accumulator.push(prod.dataset); + prod.dataset.products = [{ ...prod, dataset: null }]; + } else { + dataset[0].products.push({ ...prod, dataset: null }); + } + } + return accumulator; + }, []); +} + const list = gql` query Directory { allDiscoverableProducts(where: { environments_some: { active: true } }) { @@ -58,6 +87,7 @@ const list = gql` flow } dataset { + id name title notes @@ -74,19 +104,13 @@ const list = gql` title } } - organization { - title - } - organizationUnit { - title - } } } `; const item = gql` query GetProduct($id: ID!) { - DiscoverableProduct(where: { id: $id }) { + allDiscoverableProducts(where: { dataset: { id: $id } }) { id name environments { @@ -96,6 +120,10 @@ const item = gql` services { name host + plugins { + name + config + } } } dataset { @@ -116,12 +144,6 @@ const item = gql` title } } - organization { - title - } - organizationUnit { - title - } } } `; diff --git a/src/controllers/NamespaceController.ts b/src/controllers/NamespaceController.ts index 8e1256c87..ddf57a8af 100644 --- a/src/controllers/NamespaceController.ts +++ b/src/controllers/NamespaceController.ts @@ -9,12 +9,28 @@ import { } from 'tsoa'; import { KeystoneService } from './ioc/keystoneInjector'; import { inject, injectable } from 'tsyringe'; -import { syncRecords } from '../batch/feed-worker'; import { gql } from 'graphql-request'; -import { Namespace, Product } from '@/services/keystone/types'; -import { strict as assert } from 'assert'; +import { WorkbookService } from '../services/report/workbook.service'; +import { Namespace } from '@/services/keystone/types'; import { Logger } from '../logger'; +import { Readable } from 'stream'; + +/** + * @param binary Buffer + * returns readableInstanceStream Readable + */ +function bufferToStream(binary: any) { + const readableInstanceStream = new Readable({ + read() { + this.push(binary); + this.push(null); + }, + }); + + return readableInstanceStream; +} + const logger = Logger('controllers.Namespace'); @injectable() @@ -27,10 +43,39 @@ export class NamespaceController extends Controller { this.keystone = _keystone; } + @Get('/report') + @OperationId('report') + public async report(@Request() req: any): Promise { + const workbookService = new WorkbookService( + this.keystone.createContext(req, true) + ); + const workbook = await workbookService.buildWorkbook(); + const buffer = await workbook.xlsx.writeBuffer(); + + req.res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ); + req.res.setHeader( + 'Content-Disposition', + 'attachment; filename="bcgov_app_namespaces.xlsx"' + ); + + const mystream = bufferToStream(buffer); + mystream.pipe(req.res); + await new Promise((resolve, reject) => { + mystream.on('end', () => { + req.res.end(); + resolve(null); + }); + }); + + return null; + } + @Get() @OperationId('namespace-list') public async list(@Request() request: any): Promise { - logger.debug('Request %j', request); const result = await this.keystone.executeGraphQL({ context: this.keystone.createContext(request), query: list, diff --git a/src/controllers/ioc/keystoneInjector.ts b/src/controllers/ioc/keystoneInjector.ts index 018b7a546..d683a498d 100644 --- a/src/controllers/ioc/keystoneInjector.ts +++ b/src/controllers/ioc/keystoneInjector.ts @@ -1,6 +1,9 @@ import { Keystone } from '@keystonejs/keystone'; import { injectable } from 'tsyringe'; import { scopes, scopesToRoles } from '../../auth/scope-role-utils'; +import { Logger } from '../../logger'; + +const logger = Logger('controller'); const resolveUsername = function (user: any) { for (const nm of ['preferred_username', 'clientId']) { @@ -25,18 +28,19 @@ export class KeystoneService { return this.keystone.createContext({ skipAccessControl: true }); } - public createContext(request: any): any { + public createContext(request: any, skipAccessControl: boolean = false): any { const _scopes = scopes(request.user.scope); const identity = { id: null, username: resolveUsername(request.user), namespace: request.params.ns, - roles: scopesToRoles(_scopes), + roles: JSON.stringify(scopesToRoles(null, _scopes)), scopes: _scopes, userId: null, } as any; + logger.debug('identity %j', identity); const ctx = this.keystone.createContext({ - skipAccessControl: true, + skipAccessControl, authentication: { item: identity }, }); ctx.req = request; diff --git a/src/controllers/openapi.yaml b/src/controllers/openapi.yaml index 0408786c2..3aedd3fae 100644 --- a/src/controllers/openapi.yaml +++ b/src/controllers/openapi.yaml @@ -5,7 +5,7 @@ components: requestBodies: {} responses: {} schemas: - Content: + ContentSummary: properties: kind: type: string @@ -72,7 +72,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Content' + $ref: '#/components/schemas/ContentSummary' '/namespaces/{ns}/datasets': put: operationId: put-dataset @@ -197,6 +197,19 @@ paths: content: application/json: schema: {} + /namespaces/report: + get: + operationId: report + responses: + '200': + description: Ok + content: + application/json: + schema: {} + security: + - + jwt: [] + parameters: [] /namespaces: get: operationId: namespace-list diff --git a/src/controllers/routes.ts b/src/controllers/routes.ts index 0d6282c63..af0d78ee3 100644 --- a/src/controllers/routes.ts +++ b/src/controllers/routes.ts @@ -26,7 +26,7 @@ import * as express from 'express'; // 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 const models: TsoaRoute.Models = { - "Content": { + "ContentSummary": { "dataType": "refObject", "properties": { "kind": {"dataType":"string"}, @@ -59,7 +59,7 @@ export function RegisterRoutes(app: express.Router) { function ContentController_putContent(request: any, response: any, next: any) { const args = { ns: {"in":"path","name":"ns","required":true,"dataType":"string"}, - body: {"in":"body","name":"body","required":true,"ref":"Content"}, + body: {"in":"body","name":"body","required":true,"ref":"ContentSummary"}, request: {"in":"request","name":"request","required":true,"dataType":"object"}, }; @@ -277,6 +277,34 @@ export function RegisterRoutes(app: express.Router) { promiseHandler(controller, promise, response, undefined, next); }); // 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/namespaces/report', + authenticateMiddleware([{"jwt":[]}]), + function NamespaceController_report(request: any, response: any, next: any) { + const args = { + req: {"in":"request","name":"req","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); + } catch (err) { + return next(err); + } + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = container.get(NamespaceController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + + const promise = controller.report.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + }); + // 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/namespaces', authenticateMiddleware([{"jwt":[]}]), function NamespaceController_list(request: any, response: any, next: any) { diff --git a/src/lists/Activity.js b/src/lists/Activity.js index 29f491535..e5e44bdee 100644 --- a/src/lists/Activity.js +++ b/src/lists/Activity.js @@ -1,73 +1,77 @@ -const { Text, Checkbox, Relationship } = require('@keystonejs/fields') -const { Markdown } = require('@keystonejs/fields-markdown') +const { Text, Checkbox, Relationship } = require('@keystonejs/fields'); +const { Markdown } = require('@keystonejs/fields-markdown'); -const { byTracking, atTracking } = require('@keystonejs/list-plugins') +const { byTracking, atTracking } = require('@keystonejs/list-plugins'); -const { EnforcementPoint } = require('../authz/enforcement') +const { EnforcementPoint } = require('../authz/enforcement'); // Aidan (actor) approved (action) AccessRequest[erxAPIs for Bill #222-333-333] (type,name,refId) "Approved access request" (message) module.exports = { fields: { extRefId: { - type: Text, - isRequired: false, + type: Text, + isRequired: false, }, type: { - type: Text, - isRequired: true, + type: Text, + isRequired: true, }, name: { - type: Text, - isRequired: true, + type: Text, + isRequired: true, }, action: { - type: Text, - isRequired: true, + type: Text, + isRequired: true, }, result: { - type: Text, - isRequired: false, + type: Text, + isRequired: false, }, message: { - type: Text, - isRequired: false, + type: Text, + isRequired: false, }, context: { - type: Text, - isRequired: false, + type: Text, + isRequired: false, }, refId: { - type: Text, - isRequired: true, + type: Text, + isRequired: true, }, namespace: { - type: Text, - isRequired: false, + type: Text, + isRequired: false, }, actor: { type: Relationship, ref: 'User' }, - blob: { type: Relationship, ref: 'Blob' } + blob: { type: Relationship, ref: 'Blob' }, }, access: EnforcementPoint, - plugins: [ - atTracking() - ], + plugins: [atTracking()], hooks: { - afterChange: (async function ({ - operation, - existingItem, - originalInput, - updatedItem, - context, - listKey, - fieldPath, // exists only for field hooks - }) { - if (updatedItem.action == 'publish' && updatedItem.type == 'GatewayConfig') { - const { FeederService } = require('../services/feeder'); - const feederApi = new FeederService(process.env.FEEDER_URL) - feederApi.forceSync('kong', 'namespace', updatedItem.namespace).catch (err => { - console.log("Capture and log error " + err) - }) - } - }) - } -} + afterChange: async function ({ + operation, + existingItem, + originalInput, + updatedItem, + context, + listKey, + fieldPath, // exists only for field hooks + }) { + if ( + updatedItem.action === 'publish' && + updatedItem.type === 'GatewayConfig' && + updatedItem.result === 'completed' + ) { + const { FeederService } = require('../services/feeder'); + const feederApi = new FeederService(process.env.FEEDER_URL); + feederApi + .forceSync('kong', 'namespace', updatedItem.namespace) + .catch((err) => { + console.log('Capture and log error ' + err); + }); + } + }, + }, +}; diff --git a/src/lists/Content.js b/src/lists/Content.js index 0f40d0950..cd87632d5 100644 --- a/src/lists/Content.js +++ b/src/lists/Content.js @@ -10,6 +10,7 @@ const { const { Markdown } = require('@keystonejs/fields-markdown'); const slugify = require('slugify'); const { logger } = require('../logger'); +const { FieldEnforcementPoint } = require('../authz/enforcement'); module.exports = { labelField: 'title', @@ -44,6 +45,7 @@ module.exports = { adminConfig: { isReadOnly: false, }, + access: FieldEnforcementPoint, }, tags: { type: Text, diff --git a/src/lists/Environment.js b/src/lists/Environment.js index bf756b0cd..12af6fb3f 100644 --- a/src/lists/Environment.js +++ b/src/lists/Environment.js @@ -54,6 +54,7 @@ module.exports = { label: 'Oauth2 Client Credentials Flow', }, { value: 'kong-acl-only', label: 'Kong ACL Only' }, + { value: 'kong-api-key-only', label: 'Kong API Key Only' }, { value: 'kong-api-key-acl', label: 'Kong API Key with ACL Flow' }, ], }, diff --git a/src/lists/extensions/AliasedQueries.ts b/src/lists/extensions/AliasedQueries.ts index 10aa579be..9c8e528fd 100644 --- a/src/lists/extensions/AliasedQueries.ts +++ b/src/lists/extensions/AliasedQueries.ts @@ -45,8 +45,7 @@ module.exports = { issuers.forEach((data) => { const envDetails = JSON.parse(data.environmentDetails); envDetails.forEach(function (env: IssuerEnvironmentConfig) { - if (env.clientId || env.clientSecret) { - env.clientId = '****'; + if (env.clientSecret) { env.clientSecret = '****'; } else if (env.initialAccessToken) { env.initialAccessToken = '****'; diff --git a/src/lists/extensions/Common.ts b/src/lists/extensions/Common.ts index 858615eb9..af923d091 100644 --- a/src/lists/extensions/Common.ts +++ b/src/lists/extensions/Common.ts @@ -47,9 +47,7 @@ export interface EnvironmentContext { accessToken?: string; } -function isAuthzUsingUma2(prodEnv: { - credentialIssuer: { resourceType: string }; -}): boolean { +function isAuthzUsingUma2(prodEnv: Environment): boolean { return ( (prodEnv.credentialIssuer.resourceType == null || prodEnv.credentialIssuer.resourceType === '') == false diff --git a/src/nextapp/.eslintrc.js b/src/nextapp/.eslintrc.js index 3fdf26530..815782775 100644 --- a/src/nextapp/.eslintrc.js +++ b/src/nextapp/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { root: true, parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'react', 'jest'], + plugins: ['@typescript-eslint', 'react', 'jest', 'jsx-a11y'], extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', @@ -10,6 +10,7 @@ module.exports = { 'plugin:jest/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', ], settings: { react: { diff --git a/src/nextapp/components/access-requests/viewer.tsx b/src/nextapp/components/access-requests/viewer.tsx index 2ef7e4a29..663a66f20 100644 --- a/src/nextapp/components/access-requests/viewer.tsx +++ b/src/nextapp/components/access-requests/viewer.tsx @@ -112,6 +112,7 @@ const AccessRequestViewer: React.FC = ({ diff --git a/src/nextapp/components/actions-menu/actions-menu.stories.mdx b/src/nextapp/components/actions-menu/actions-menu.stories.mdx new file mode 100644 index 000000000..9ff08019d --- /dev/null +++ b/src/nextapp/components/actions-menu/actions-menu.stories.mdx @@ -0,0 +1,37 @@ +import { + Box, + MenuItem +} from '@chakra-ui/react'; +import { Meta, Story, Canvas } from '@storybook/addon-docs'; +import ActionsMenu from './actions-menu'; + + + +# Actions Menu + +The `ActionsMenu` is a convenient wrapper around the Charka Menu, ideal for use in list-based data displays. + +To use + +```javascript +import { MenuItem } from '@chakra-ui/react'; +import ActionsMenu from '@src/components/actions-menu'; + +const MyComponent = () => ( + + /* handle click */}>Delete + +); +``` + + + + + Delete + + + + diff --git a/src/nextapp/components/actions-menu/actions-menu.tsx b/src/nextapp/components/actions-menu/actions-menu.tsx new file mode 100644 index 000000000..54f1b14c9 --- /dev/null +++ b/src/nextapp/components/actions-menu/actions-menu.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { Icon, Menu, MenuButton, MenuList, MenuProps } from '@chakra-ui/react'; +import { IoEllipsisHorizontal } from 'react-icons/io5'; + +interface ActionsMenuProps extends MenuProps { + children: React.ReactNode; +} + +const ActionsMenu: React.FC = ({ children, ...rest }) => { + return ( + + + + + {children} + + ); +}; + +export default ActionsMenu; diff --git a/src/nextapp/components/actions-menu/index.ts b/src/nextapp/components/actions-menu/index.ts new file mode 100644 index 000000000..15cf64a56 --- /dev/null +++ b/src/nextapp/components/actions-menu/index.ts @@ -0,0 +1 @@ +export { default } from './actions-menu'; diff --git a/src/nextapp/components/api-product-item/api-product-item.tsx b/src/nextapp/components/api-product-item/api-product-item.tsx new file mode 100644 index 000000000..95402a8ea --- /dev/null +++ b/src/nextapp/components/api-product-item/api-product-item.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { BiLinkExternal } from 'react-icons/bi'; +import { + Button, + Box, + Flex, + Grid, + GridItem, + Heading, + Icon, + Link, + Text, +} from '@chakra-ui/react'; +import { FaLock } from 'react-icons/fa'; +import { HiChartBar } from 'react-icons/hi'; +import NextLink from 'next/link'; +import { RiEarthFill } from 'react-icons/ri'; +import { Dataset, Environment, Product } from '@/shared/types/query.types'; + +export interface ApiEnvironment extends Environment { + anonymous: boolean; +} + +export interface ApiProduct extends Product { + environments: ApiEnvironment[]; +} + +export interface ApiDataset extends Dataset { + products: ApiProduct[]; +} + +export interface ApiProductItemProps { + data: ApiProduct; + id: string; +} + +const ApiProductItem: React.FC = ({ data, id }) => { + const isPublic = data.environments.some((e) => e.flow === 'public'); + const isTiered = data.environments.some((e) => e.anonymous); + const accessLink = `/devportal/requests/new/${id}`; + + return ( + <> + {' '} + + + + + + + + {data.name} + + {data.description && ( + + {data.description} + + )} + + + {!isPublic && !isTiered && ( + + + + )} + + {isTiered && ( + + + + + + + + Limits + + {data.description && ( + + Public access has a rate limit enforced. + + )} + + For elevated access, please{' '} + + Request Access + + + + + + )} + + ); +}; + +export default ApiProductItem; diff --git a/src/nextapp/components/api-product-item/index.ts b/src/nextapp/components/api-product-item/index.ts new file mode 100644 index 000000000..2aaf8d5ad --- /dev/null +++ b/src/nextapp/components/api-product-item/index.ts @@ -0,0 +1,2 @@ +export { default } from './api-product-item'; +export type { ApiDataset } from './api-product-item'; diff --git a/src/nextapp/components/application-services/application-services.tsx b/src/nextapp/components/application-services/application-services.tsx new file mode 100644 index 000000000..b411be56c --- /dev/null +++ b/src/nextapp/components/application-services/application-services.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { ListItem, UnorderedList } from '@chakra-ui/layout'; +import { useApi } from '@/shared/services/api'; +import { gql } from 'graphql-request'; +import { uid } from 'react-uid'; + +interface ApplicationServicesProps { + appId: string; +} + +const ApplicationServices: React.FC = ({ appId }) => { + const { data } = useApi(['applicationServices', appId], { + query, + variables: { appId }, + }); + + return ( + + {data.myServiceAccesses.length === 0 && ( + No Service Accesses + )} + {data.myServiceAccesses.map((s) => ( + + {s.name} + + ))} + + ); +}; + +export default ApplicationServices; + +const query = gql` + query GET_APPLICATION_SERVICES($appId: String!) { + myServiceAccesses(where: { application: { appId: $appId } }) { + id + name + active + application { + name + } + productEnvironment { + id + name + product { + name + } + } + } + } +`; diff --git a/src/nextapp/components/application-services/index.ts b/src/nextapp/components/application-services/index.ts new file mode 100644 index 000000000..e8fcaae35 --- /dev/null +++ b/src/nextapp/components/application-services/index.ts @@ -0,0 +1 @@ +export { default } from './application-services'; diff --git a/src/nextapp/components/auth-action/auth-action.tsx b/src/nextapp/components/auth-action/auth-action.tsx index 3989ccd17..eeac8ee87 100644 --- a/src/nextapp/components/auth-action/auth-action.tsx +++ b/src/nextapp/components/auth-action/auth-action.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { + Avatar, Box, Button, Icon, @@ -7,10 +8,12 @@ import { MenuButton, MenuList, MenuItem, + Center, + HStack, + StackDivider, } from '@chakra-ui/react'; -import { FaChevronDown, FaUserCircle } from 'react-icons/fa'; +import { FaChevronDown } from 'react-icons/fa'; import { useAuth } from '@/shared/services/auth'; -import { useRouter } from 'next/router'; import NamespaceMenu from '../namespace-menu'; interface AuthActionProps { @@ -19,9 +22,6 @@ interface AuthActionProps { const Signin: React.FC = ({ site }) => { const { user } = useAuth(); - const router = useRouter(); - - const onNextLinkClick = (event) => router.push(event.target.value); if (site === 'redirect') { return <>; @@ -41,7 +41,12 @@ const Signin: React.FC = ({ site }) => { } return ( - + + } + spacing={4} + > {user.roles.includes('portal-user') && } = ({ site }) => { position="relative" zIndex={2} > - + - - {user.name} - +
+ + +
My Profile @@ -83,7 +96,7 @@ const Signin: React.FC = ({ site }) => {
- + ); }; diff --git a/src/nextapp/components/card/card.stories.mdx b/src/nextapp/components/card/card.stories.mdx new file mode 100644 index 000000000..9af971920 --- /dev/null +++ b/src/nextapp/components/card/card.stories.mdx @@ -0,0 +1,136 @@ +import { + Box, + Button, + Table, + Thead, + Tbody, + Tfoot, + Tr, + Th, + Td, + TableCaption +} from '@chakra-ui/react'; +import { Meta, Story, Canvas } from '@storybook/addon-docs'; +import Card from './card'; + + + +# Card + +A card component at its most basic implementation is a simple white box, but it can be expanded to present tables with headings and actions. + +```javascript +import Card from '@src/components/card'; + +const MyComponent = () => ( + + Card Content + +); +``` + + + + Vanilla Card + + + +Note: When rendering content not a table, compose the root child component to have this basic composure + +```javascript + + + {...} + + +``` + +## Heading variant + +Headings most commonly can be passed in as strings, but for more complex components a `ReactNode` can be passed in. Note they will be rendered inside a `Heading` component. + + + + + + + + + +## With Content + +The most common content for the card is a `Table`, which can be composed normally as `children` of the `Card` + + + + + + Imperial to metric conversion factors + + + + + + + + + + + + + + + + + + + + + + + + +
To convertintomultiply by
inchesmillimetres (mm)25.4
feetcentimetres (cm)30.48
yardsmetres (m)0.91444
+
+
+
+ +## Card Actions + +Buttons can be passed into the `actions` prop, any group of components will be spaced out in a `HStack` component. + + + + + + + + } + heading="All Activities" + > + Content + + + + +## Extending the Card component + +The card component can accept all `Box` props. See [Box component documentation](https://chakra-ui.com/docs/layout/box) for more + + + + + Content + + + + diff --git a/src/nextapp/components/card/card.stories.tsx b/src/nextapp/components/card/card.stories.tsx deleted file mode 100644 index e90d03d39..000000000 --- a/src/nextapp/components/card/card.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { VStack } from '@chakra-ui/react'; - -import Card from './card'; - -export default { - title: 'APS/Card', -}; - -export const DefaultCard = () => ( - - Sample Card - -); diff --git a/src/nextapp/components/card/card.tsx b/src/nextapp/components/card/card.tsx index c163fe0c7..5a4929231 100644 --- a/src/nextapp/components/card/card.tsx +++ b/src/nextapp/components/card/card.tsx @@ -1,24 +1,45 @@ import * as React from 'react'; -import { Box, Flex } from '@chakra-ui/react'; +import { + Box, + BoxProps, + Flex, + Heading, + HStack, + useStyleConfig, +} from '@chakra-ui/react'; -interface CardProps { +interface CardProps extends BoxProps { + actions?: React.ReactNode; children: React.ReactNode; + heading?: React.ReactNode; } -const Card: React.FC = ({ children }) => { +const Card: React.FC = ({ + actions, + children, + heading, + ...props +}) => { + const styles = useStyleConfig('Box'); + return ( - + + {heading && ( + + {heading} + {actions && {actions}} + + )} {children} - + ); }; diff --git a/src/nextapp/components/confirmation-dialog/confirmation-dialog.tsx b/src/nextapp/components/confirmation-dialog/confirmation-dialog.tsx new file mode 100644 index 000000000..1c31d3391 --- /dev/null +++ b/src/nextapp/components/confirmation-dialog/confirmation-dialog.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { + AlertDialog, + AlertDialogBody, + AlertDialogCloseButton, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Box, + BoxProps, + Button, + useDisclosure, +} from '@chakra-ui/react'; + +interface ConfirmationDialogProps { + body: string; + children: React.ReactNode; + confirmButtonText?: string; + containerProps?: BoxProps; + destructive?: boolean; + onConfirm: () => void; + title: string; +} + +const ConfirmationDialog: React.FC = ({ + body, + children, + confirmButtonText = 'Yes', + containerProps = {}, + destructive, + onConfirm, + title, +}) => { + const cancelRef = React.useRef(); + const { isOpen, onClose, onOpen } = useDisclosure(); + + const handleConfirm = React.useCallback(() => { + onClose(); + onConfirm(); + }, [onClose, onConfirm]); + + return ( + <> + + {children} + + + + + {title} + + {body} + + + + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/src/nextapp/components/confirmation-dialog/index.ts b/src/nextapp/components/confirmation-dialog/index.ts new file mode 100644 index 000000000..973f4b925 --- /dev/null +++ b/src/nextapp/components/confirmation-dialog/index.ts @@ -0,0 +1 @@ +export { default } from './confirmation-dialog'; diff --git a/src/nextapp/components/controls-list/controls-list.tsx b/src/nextapp/components/controls-list/controls-list.tsx index c5f9d9ad0..6c650344f 100644 --- a/src/nextapp/components/controls-list/controls-list.tsx +++ b/src/nextapp/components/controls-list/controls-list.tsx @@ -18,13 +18,19 @@ import ModelIcon from '../model-icon/model-icon'; import IpRestriction from '../controls/ip-restriction'; import RateLimiting from '../controls/rate-limiting'; import DeleteControl from './delete-control'; +import { QueryKey } from 'react-query'; interface ControlsListProps { consumerId: string; data: GatewayPlugin[]; + queryKey: QueryKey; } -const ControlsList: React.FC = ({ consumerId, data }) => { +const ControlsList: React.FC = ({ + consumerId, + data, + queryKey, +}) => { const getIcon = React.useCallback((name: string) => { switch (name) { case 'rate-limiting': @@ -89,7 +95,7 @@ const ControlsList: React.FC = ({ consumerId, data }) => { )} @@ -97,13 +103,14 @@ const ControlsList: React.FC = ({ consumerId, data }) => { )} @@ -147,6 +154,7 @@ const ControlsList: React.FC = ({ consumerId, data }) => { diff --git a/src/nextapp/components/controls-list/delete-control.tsx b/src/nextapp/components/controls-list/delete-control.tsx index d50c47fc9..6f48aaadd 100644 --- a/src/nextapp/components/controls-list/delete-control.tsx +++ b/src/nextapp/components/controls-list/delete-control.tsx @@ -15,17 +15,19 @@ import { import { gql } from 'graphql-request'; import * as React from 'react'; import { FaTrash } from 'react-icons/fa'; -import { useQueryClient } from 'react-query'; +import { QueryKey, useQueryClient } from 'react-query'; import { useApiMutation } from '@/shared/services/api'; interface DeleteControlProps { consumerId: string; pluginExtForeignKey: string; + queryKey: QueryKey; } const DeleteControl: React.FC = ({ consumerId, pluginExtForeignKey, + queryKey, }) => { const toast = useToast(); const queryClient = useQueryClient(); @@ -39,7 +41,7 @@ const DeleteControl: React.FC = ({ const handleDelete = async () => { try { await deleteMutation.mutateAsync({ consumerId, pluginExtForeignKey }); - queryClient.invalidateQueries(['consumer', consumerId]); + queryClient.invalidateQueries(queryKey); toast({ title: 'Control removed', status: 'success', diff --git a/src/nextapp/components/discovery-list/discovery-list-item.tsx b/src/nextapp/components/discovery-list/discovery-list-item.tsx index f135af784..d0a862fc9 100644 --- a/src/nextapp/components/discovery-list/discovery-list-item.tsx +++ b/src/nextapp/components/discovery-list/discovery-list-item.tsx @@ -12,11 +12,11 @@ import { WrapItem, } from '@chakra-ui/react'; import NextLink from 'next/link'; -import { Product } from '@/shared/types/query.types'; +import { Dataset, Product } from '@/shared/types/query.types'; import { FaBook } from 'react-icons/fa'; interface DiscoveryListItemProps { - data: Product; + data: Dataset; } const DiscoveryListItem: React.FC = ({ data }) => { @@ -43,10 +43,10 @@ const DiscoveryListItem: React.FC = ({ data }) => { - {data.dataset ? ( + {data ? ( <> - {data.dataset.title} + {data.title} ) : ( @@ -60,10 +60,10 @@ const DiscoveryListItem: React.FC = ({ data }) => { - {data.dataset?.organization && ( + {data.organization && ( <> - {data.dataset.organization.title} - {data.dataset.organizationUnit && ( + {data.organization.title} + {data.organizationUnit && ( <> = ({ data }) => { mt={1} fontSize="xs" > - {data.dataset.organizationUnit.title} + {data.organizationUnit.title} )} )} - {!data.dataset?.organization && 'Open Dataset'} + {!data.organization && 'Open Dataset'} - {data.dataset && ( + {data && ( - {data.dataset.notes} + {data.notes} )} - {!data.dataset && ( + {!data && ( No description added @@ -94,17 +94,15 @@ const DiscoveryListItem: React.FC = ({ data }) => { - - {data.dataset && ( - {data.dataset.sector} - )} - + {data && {data.sector}} - {data.environments.map((e) => ( - - {e.name} - - ))} + {(data as any).products.map((prod: Product) => + prod.environments.map((e) => ( + + {e.name} + + )) + )} diff --git a/src/nextapp/components/discovery-list/discovery-list.tsx b/src/nextapp/components/discovery-list/discovery-list.tsx index 69c7295e7..bbe543d3c 100644 --- a/src/nextapp/components/discovery-list/discovery-list.tsx +++ b/src/nextapp/components/discovery-list/discovery-list.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { Grid } from '@chakra-ui/react'; -import { Product } from '@/shared/types/query.types'; +import { Dataset, Product } from '@/shared/types/query.types'; import DiscoveryListItem from './discovery-list-item'; interface DiscoveryListProps { - data: Product[]; + data: Dataset[]; } const DiscoveryList: React.FC = ({ data }) => { diff --git a/src/nextapp/components/empty-pane/empty-pane.tsx b/src/nextapp/components/empty-pane/empty-pane.tsx index 7ec93c934..e7d1d6e30 100644 --- a/src/nextapp/components/empty-pane/empty-pane.tsx +++ b/src/nextapp/components/empty-pane/empty-pane.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { Box, Center, Heading, Icon, Text } from '@chakra-ui/react'; -import { FaRegFolderOpen } from 'react-icons/fa'; +import { Box, Center, Heading, Text } from '@chakra-ui/react'; interface EmptyPaneProps { action?: React.ReactNode; @@ -22,16 +21,15 @@ const EmptyPane: React.FC = ({ p={8} bg="white" borderRadius="4px" - maxW={{ sm: 400 }} + maxW={{ sm: 500 }} mx={{ base: 4 }} > - {title} {message} {error && {error}} - {action && {action}} + {action && {action}} ); diff --git a/src/nextapp/components/environment-config/environment-config.tsx b/src/nextapp/components/environment-config/environment-config.tsx index 349c14736..f25920d05 100644 --- a/src/nextapp/components/environment-config/environment-config.tsx +++ b/src/nextapp/components/environment-config/environment-config.tsx @@ -42,6 +42,7 @@ const EnvironmentConfig: React.FC = ({ data = {} }) => { { value: 'authorization-code', label: 'Oauth2 Authorization Code Flow' }, { value: 'client-credentials', label: 'Oauth2 Client Credentials Flow' }, { value: 'kong-acl-only', label: 'Kong ACL Only' }, + { value: 'kong-api-key-only', label: 'Kong API Key Only' }, { value: 'kong-api-key-acl', label: 'Kong API Key with ACL Flow' }, ]; diff --git a/src/nextapp/components/environment-plugins/environment-plugins.tsx b/src/nextapp/components/environment-plugins/environment-plugins.tsx index d025211c1..f58179564 100644 --- a/src/nextapp/components/environment-plugins/environment-plugins.tsx +++ b/src/nextapp/components/environment-plugins/environment-plugins.tsx @@ -6,6 +6,7 @@ import YamlViewer from '../yaml-viewer'; import JwtKeycloak from './templates/jwt-keycloak'; import KongAclOnly from './templates/kong-acl-only'; import KongApiKeyAcl from './templates/kong-api-key-acl'; +import KongApiKeyOnly from './templates/kong-api-key-only'; import { gql } from 'graphql-request'; import { useApi } from '@/shared/services/api'; @@ -47,6 +48,7 @@ const EnvironmentPlugins: React.FC = ({ data }) => { const pluginConfigs = { 'kong-api-key-acl': KongApiKeyAcl(data.product.namespace, data.appId), + 'kong-api-key-only': KongApiKeyOnly(data.product.namespace, data.appId), 'kong-acl-only': KongAclOnly(data.product.namespace, data.appId), 'client-credentials': JwtKeycloak( data.product.namespace, diff --git a/src/nextapp/components/environment-plugins/templates/kong-api-key-only.ts b/src/nextapp/components/environment-plugins/templates/kong-api-key-only.ts new file mode 100644 index 000000000..d2b80e3fd --- /dev/null +++ b/src/nextapp/components/environment-plugins/templates/kong-api-key-only.ts @@ -0,0 +1,13 @@ +export default function KongApiKeyAcl(namespace, appId) { + return ` + plugins: + - name: key-auth + tags: [ ns.${namespace} ] + protocols: [ http, https ] + config: + key_names: ["X-API-KEY"] + run_on_preflight: true + hide_credentials: true + key_in_body: false +`; +} diff --git a/src/nextapp/components/environments-list/edit-environment.tsx b/src/nextapp/components/environments-list/edit-environment.tsx index 69d4534c2..c8225c689 100644 --- a/src/nextapp/components/environments-list/edit-environment.tsx +++ b/src/nextapp/components/environments-list/edit-environment.tsx @@ -38,6 +38,7 @@ const EditEnvironment: React.FC = ({ data }) => { { value: 'authorization-code', label: 'Oauth2 Authorization Code Flow' }, { value: 'client-credentials', label: 'Oauth2 Client Credentials Flow' }, { value: 'kong-acl-only', label: 'Kong ACL Only' }, + { value: 'kong-api-key-only', label: 'Kong API Key Only' }, { value: 'kong-api-key-acl', label: 'Kong API Key with ACL Flow' }, ]; diff --git a/src/nextapp/components/header/header.tsx b/src/nextapp/components/header/header.tsx index 99b9029d6..6f2dd25d2 100644 --- a/src/nextapp/components/header/header.tsx +++ b/src/nextapp/components/header/header.tsx @@ -48,14 +48,25 @@ const Header: React.FC = ({ site, children }) => { flexGrow={{ base: 1, sm: 0 }} > - + <> - + BC Government Logo - + Government of British Columbia - + API {site == 'manager' ? 'Provider Console' : 'Services Portal'} diff --git a/src/nextapp/components/namespace-manager/namespace-manager.tsx b/src/nextapp/components/namespace-manager/namespace-manager.tsx index 3eeff0137..1138c88ea 100644 --- a/src/nextapp/components/namespace-manager/namespace-manager.tsx +++ b/src/nextapp/components/namespace-manager/namespace-manager.tsx @@ -12,13 +12,15 @@ import { Box, Text, Flex, + Link, IconButton, Divider, + Spacer, Center, Heading, } from '@chakra-ui/react'; import type { NamespaceData } from '@/shared/types/app.types'; -import { FaTrash } from 'react-icons/fa'; +import { FaTrash, FaDownload } from 'react-icons/fa'; import NamespaceDelete from '../namespace-delete'; @@ -111,7 +113,12 @@ const NamespaceManager: React.FC = ({ - + + diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 0cc365fdb..57295d8d4 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -71,18 +71,15 @@ const NamespaceMenu: React.FC = ({ user }) => { - - {user?.namespace ?? 'No Active Namespace'} + {user?.namespace ?? 'No Active Namespace'}{' '} + <> diff --git a/src/nextapp/components/new-application/new-application-dialog.tsx b/src/nextapp/components/new-application/new-application-dialog.tsx index 61ad53792..4970251ed 100644 --- a/src/nextapp/components/new-application/new-application-dialog.tsx +++ b/src/nextapp/components/new-application/new-application-dialog.tsx @@ -3,6 +3,7 @@ import { Button, ButtonGroup, FormControl, + FormHelperText, FormLabel, Input, Modal, @@ -89,17 +90,14 @@ const NewApplicationDialog: React.FC = ({ Create Application
- + Application Name - + - - Description -