From b758f152bc3eb38344d4962e1f236062c851ac13 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 13:01:02 -0700 Subject: [PATCH 01/21] fix ofor seeding Gateway Services product --- src/batch/data-rules.ts | 2 +- src/services/keystone/gateway-service.ts | 33 +- .../workflow/validate-active-environment.ts | 7 +- .../validate-active-environment.test.js | 410 ++++++++++-------- 4 files changed, 265 insertions(+), 187 deletions(-) diff --git a/src/batch/data-rules.ts b/src/batch/data-rules.ts index baefa3906..87c4e91ba 100644 --- a/src/batch/data-rules.ts +++ b/src/batch/data-rules.ts @@ -310,7 +310,7 @@ export const metadata = { Product: { query: 'allProducts', refKey: 'appId', - sync: ['name'], + sync: ['name', 'namespace'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, environments: { diff --git a/src/services/keystone/gateway-service.ts b/src/services/keystone/gateway-service.ts index 1798c0d9c..0b14fcbdc 100644 --- a/src/services/keystone/gateway-service.ts +++ b/src/services/keystone/gateway-service.ts @@ -1,11 +1,17 @@ -import { Logger } from '../../logger' -import { GatewayService } from './types' +import { Logger } from '../../logger'; +import { GatewayService } from './types'; -const logger = Logger('keystone.gw-service') +const logger = Logger('keystone.gw-service'); -export async function lookupServices (context: any, serviceIds: string[]) : Promise { - const result = await context.executeGraphQL({ - query: `query GetServices($services: [ID]) { +export async function lookupServices( + context: any, + serviceIds: string[] +): Promise { + if (serviceIds.length == 0) { + return []; + } + const result = await context.executeGraphQL({ + query: `query GetServices($services: [ID]) { allGatewayServices(where: {id_in: $services}) { name plugins { @@ -21,10 +27,11 @@ export async function lookupServices (context: any, serviceIds: string[]) : Prom } } }`, - variables: { services: serviceIds }, - }) - logger.debug("Query result %j", result) - result.data.allGatewayServices.map((svc:GatewayService) => - svc.plugins?.map(plugin => plugin.config = JSON.parse(plugin.config))) - return result.data.allGatewayServices -} \ No newline at end of file + variables: { services: serviceIds }, + }); + logger.debug('Query result %j', result); + result.data.allGatewayServices.map((svc: GatewayService) => + svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))) + ); + return result.data.allGatewayServices; +} diff --git a/src/services/workflow/validate-active-environment.ts b/src/services/workflow/validate-active-environment.ts index 8b9291715..82cae1e83 100644 --- a/src/services/workflow/validate-active-environment.ts +++ b/src/services/workflow/validate-active-environment.ts @@ -46,6 +46,9 @@ export const ValidateActiveEnvironment = async ( const flow = existingItem == null ? resolvedData['flow'] : envServices.flow; + const envName = + existingItem == null ? resolvedData['name'] : envServices.name; + // The Credential Issuer says what plugins are expected // Loop through the Services to make sure the plugin is configured correctly @@ -94,7 +97,7 @@ export const ValidateActiveEnvironment = async ( 'Environment missing issuer details' ); - const envConfig = getIssuerEnvironmentConfig(issuer, envServices.name); + const envConfig = getIssuerEnvironmentConfig(issuer, envName); const isServiceMissingAllPlugins = (svc: any) => svc.plugins.filter( @@ -124,7 +127,7 @@ export const ValidateActiveEnvironment = async ( 'Environment missing issuer details' ); - const envConfig = getIssuerEnvironmentConfig(issuer, envServices.name); + const envConfig = getIssuerEnvironmentConfig(issuer, envName); const isServiceMissingAllPlugins = (svc: any) => svc.plugins.filter( diff --git a/src/test/services/workflow/validate-active-environment.test.js b/src/test/services/workflow/validate-active-environment.test.js index e99e48437..2ed3c525d 100644 --- a/src/test/services/workflow/validate-active-environment.test.js +++ b/src/test/services/workflow/validate-active-environment.test.js @@ -1,12 +1,11 @@ import setup from './setup'; -import workflow from '../../../services/workflow'; +import { ValidateActiveEnvironment } from '../../../services/workflow'; -import { json } from './utils' +import { json } from './utils'; describe('Validate Active Environment', function () { - - const ctx = setup() + const ctx = setup(); // Enable API mocking before tests. beforeAll(() => ctx.server.listen()); @@ -14,68 +13,84 @@ describe('Validate Active Environment', function () { // Reset any runtime request handlers we may add during the tests. afterEach(() => ctx.server.resetHandlers()); - beforeEach(() => { - ctx.context.OUTPUTS = []; + beforeEach(() => { + ctx.context.OUTPUTS = []; ctx.context.Activity = []; ctx.context.IN = { - GetProductEnvironmentServices: { - data: { - allEnvironments: [ - { - name: 'ENV-NAME-1', - flow: 'kong-api-key-acl', - services: [ - { - name: 'SERVICE-1', - plugins: [ - { - name: 'acl', - config: '{}', - }, - { - name: 'key-auth', - config: '{}', - }, - ], - routes: [ - { - name: 'SERVICE-ROUTE-1', - plugins: [ - { - name: 'rate-limiting', - config: '{}', - }, - ], - }, - ], - }, - ], + GetProductEnvironmentServices: { + data: { + allEnvironments: [ + { + name: 'ENV-NAME-1', + flow: 'kong-api-key-acl', + credentialIssuer: { + id: 'ci-123', + flow: 'client-credentials', + environmentDetails: JSON.stringify([ + { environment: 'ENV-NAME-1', issuerUrl: 'http://provider' }, + ]), }, - ], + services: [ + { + name: 'SERVICE-1', + plugins: [ + { + name: 'acl', + config: '{}', + }, + { + name: 'key-auth', + config: '{}', + }, + ], + routes: [ + { + name: 'SERVICE-ROUTE-1', + plugins: [ + { + name: 'rate-limiting', + config: '{}', + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + GetCredentialIssuerById: { + data: { + CredentialIssuer: { + id: 'ci-123', + flow: 'client-credentials', + name: 'Auth Profile 123', + mode: 'auto', + environmentDetails: JSON.stringify([ + { environment: 'ENV-NAME-1', issuerUrl: 'http://provider' }, + ]), }, }, - - }; - - - }) + }, + }; + }); // Disable API mocking after the tests are done. afterAll(() => ctx.server.close()); describe('validate active environment for api key flow', function () { - it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -85,67 +100,72 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing acl plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true + const params = { + existingItem: { + id: 'ENV-1', + active: true, }, - originalInput: {}, - resolvedInput: {}, - }; - - ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0].services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete acl or key-auth plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + originalInput: {}, + resolvedInput: {}, + }; + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0].services[0].plugins = []; + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: + '[SERVICE-1] missing or incomplete acl or key-auth plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); }); describe('validate active environment for client-credential flow', function () { - it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'client-credentials' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [{ name: "jwt-keycloak", config: JSON.stringify({ well_known_template: "http://provider/realm/.well-known/openid-configuration"}) }] + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'client-credentials'; + prodEnv.credentialIssuer.flow = 'client-credentials'; + + prodEnv.services[0].plugins = [ + { + name: 'jwt-keycloak', + config: JSON.stringify({ + well_known_template: + 'http://provider/realm/.well-known/openid-configuration', + }), + }, + ]; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -155,70 +175,121 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing acl plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true - }, - originalInput: {}, - resolvedInput: {}, - }; - - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'client-credentials' - prodEnv.services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete jwt-keycloak plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + const params = { + existingItem: { + id: 'ENV-1', + active: true, + }, + originalInput: {}, + resolvedInput: {}, + }; + + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'client-credentials'; + prodEnv.services[0].plugins = []; + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: '[SERVICE-1] missing or incomplete jwt-keycloak plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); }); + describe('validate create active environment for client-credential flow', function () { + it('it should succeed', async function () { + const params = { + existingItem: null, + originalInput: { + active: true, + }, + resolvedInput: { + credentialIssuer: 1, + name: 'ENV-NAME-1', + flow: 'client-credentials', + services: [], + }, + }; - describe('validate active environment for authorization-code flow', function () { + // const prodEnv = + // ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + // prodEnv.flow = 'client-credentials'; + // prodEnv.credentialIssuer = { + // oidcDiscoveryUrl: + // 'http://provider/realm/.well-known/openid-configuration', + // }; + // prodEnv.services[0].plugins = [ + // { + // name: 'jwt-keycloak', + // config: JSON.stringify({ + // well_known_template: + // 'http://provider/realm/.well-known/openid-configuration', + // }), + // }, + // ]; + + await ValidateActiveEnvironment( + ctx.context, + 'create', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + const expected = { + OUTPUTS: [], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); + }); + + describe('validate active environment for authorization-code flow', function () { it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'authorization-code' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [{ name: "oidc", config: JSON.stringify({ discovery: "http://provider/realm/.well-known/openid-configuration"}) }] + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'authorization-code'; + prodEnv.credentialIssuer.flow = 'authorization-code'; + + prodEnv.services[0].plugins = [ + { + name: 'oidc', + config: JSON.stringify({ + discovery: 'http://provider/realm/.well-known/openid-configuration', + }), + }, + ]; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -228,49 +299,46 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing oidc plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true - }, - originalInput: {}, - resolvedInput: {}, - }; - - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'authorization-code' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete oidc plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + const params = { + existingItem: { + id: 'ENV-1', + active: true, + }, + originalInput: {}, + resolvedInput: {}, + }; + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'authorization-code'; + prodEnv.credentialIssuer.flow = 'authorization-code'; - }); + prodEnv.services[0].plugins = []; + + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: '[SERVICE-1] missing or incomplete oidc plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); + }); }); From 2ac0ca8a8f1885e9af831b1ed16af48a233280bf Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 13:04:13 -0700 Subject: [PATCH 02/21] remove some commented code --- .../validate-active-environment.test.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/test/services/workflow/validate-active-environment.test.js b/src/test/services/workflow/validate-active-environment.test.js index 2ed3c525d..f6a22d2e7 100644 --- a/src/test/services/workflow/validate-active-environment.test.js +++ b/src/test/services/workflow/validate-active-environment.test.js @@ -231,23 +231,6 @@ describe('Validate Active Environment', function () { }, }; - // const prodEnv = - // ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; - // prodEnv.flow = 'client-credentials'; - // prodEnv.credentialIssuer = { - // oidcDiscoveryUrl: - // 'http://provider/realm/.well-known/openid-configuration', - // }; - // prodEnv.services[0].plugins = [ - // { - // name: 'jwt-keycloak', - // config: JSON.stringify({ - // well_known_template: - // 'http://provider/realm/.well-known/openid-configuration', - // }), - // }, - // ]; - await ValidateActiveEnvironment( ctx.context, 'create', From 8bdf9987e781aec487eb2638f9a50c56f102dfb9 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Wed, 16 Jun 2021 16:12:35 -0700 Subject: [PATCH 03/21] Fix namespace switching for create/delete. --- .../components/namespace-delete/namespace-delete.tsx | 4 ++-- src/nextapp/components/new-namespace/new-namespace.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/namespace-delete/namespace-delete.tsx b/src/nextapp/components/namespace-delete/namespace-delete.tsx index 6ad03694d..0bb44cd85 100644 --- a/src/nextapp/components/namespace-delete/namespace-delete.tsx +++ b/src/nextapp/components/namespace-delete/namespace-delete.tsx @@ -13,7 +13,7 @@ import { } from '@chakra-ui/react'; import { useQueryClient } from 'react-query'; import { gql } from 'graphql-request'; -import { useApiMutation } from '@/shared/services/api'; +import { restApi, useApiMutation } from '@/shared/services/api'; import { useAuth } from '@/shared/services/auth'; import { useRouter } from 'next/router'; @@ -40,6 +40,7 @@ const NamespaceDelete: React.FC = ({ if (user.namespace === name && router) { router.push('/manager'); + await restApi('/admin/switch', { method: 'PUT' }); } toast({ @@ -96,4 +97,3 @@ const mutation = gql` deleteNamespace(namespace: $name) } `; - diff --git a/src/nextapp/components/new-namespace/new-namespace.tsx b/src/nextapp/components/new-namespace/new-namespace.tsx index ef862809a..c1c02fbb7 100644 --- a/src/nextapp/components/new-namespace/new-namespace.tsx +++ b/src/nextapp/components/new-namespace/new-namespace.tsx @@ -16,7 +16,7 @@ import { import { useMutation, useQueryClient } from 'react-query'; import { gql } from 'graphql-request'; import { restApi, useApiMutation } from '@/shared/services/api'; -import type { Mutation } from '@/types/query.types' +import type { Mutation } from '@/types/query.types'; interface NewNamespace { isOpen: boolean; onClose: () => void; @@ -40,12 +40,15 @@ const NewNamespace: React.FC = ({ isOpen, onClose }) => { const name = data.get('name') as string; const json: Mutation = await createMutation.mutateAsync({ name, - }) + }); toast({ title: `Namespace ${json.createNamespace.name} created!`, status: 'success', }); + await restApi(`/admin/switch/${json.createNamespace.id}`, { + method: 'PUT', + }); queryClient.invalidateQueries(); toast({ title: `Switched to ${json.createNamespace.name} namespace`, From 9999ecfefedd62085bf52fced3403924488bcc6f Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Thu, 17 Jun 2021 17:03:33 -0700 Subject: [PATCH 04/21] Fix tags bleeding out of table in namespace access --- .../inline-permissions-list.tsx | 16 ++-- .../pages/manager/namespace-access/index.tsx | 77 ++++++++++++------- src/test/mock-server/server.js | 2 + 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx index 504736df0..83bde4fd3 100644 --- a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx +++ b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { HStack, Tag, TagCloseButton } from '@chakra-ui/react'; +import { Tag, TagCloseButton, Wrap, WrapItem } from '@chakra-ui/react'; interface InlinePermissionsListProps { data: { @@ -24,14 +24,16 @@ const InlinePermissionsList: React.FC = ({ ); return ( - + {data.map((p) => ( - - {p.scopeName} - {enableRevoke && } - + + + {p.scopeName} + {enableRevoke && } + + ))} - + ); }; diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index a8eb6115a..31d8c6f1d 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -1,11 +1,57 @@ import * as React from 'react'; -import graphql from '@/shared/services/graphql'; import { gql } from 'graphql-request'; -const { useEffect, useState } = React; - import ResourceAccess from './resource-access'; +import api, { useApi } from '@/shared/services/api'; +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import { QueryClient } from 'react-query'; +import { Query } from '@/shared/types/query.types'; +import { dehydrate } from 'react-query/hydration'; + +export const getServerSideProps: GetServerSideProps = async (context) => { + const queryKey = 'namespaceAccess'; + const queryClient = new QueryClient(); + + await queryClient.prefetchQuery( + queryKey, + async () => + await api(query, { + headers: context.req.headers as HeadersInit, + }) + ); + + return { + props: { + dehydratedState: dehydrate(queryClient), + queryKey, + }, + }; +}; + +const AccessRedirectPage: React.FC< + InferGetServerSidePropsType +> = ({ queryKey }) => { + const { data } = useApi( + queryKey, + { query }, + { + suspense: false, + } + ); + + return ( + + ); +}; -const GET_CURRENT_NS = gql` +export default AccessRedirectPage; + +const query = gql` query GET { currentNamespace { id @@ -17,26 +63,3 @@ const GET_CURRENT_NS = gql` } } `; - -const AccessRedirectPage = () => { - const [data, setData] = useState(null); - const fetch = () => { - graphql(GET_CURRENT_NS).then(({ data }) => { - setData(data.currentNamespace); - }); - }; - - useEffect(fetch, []); - - if (data != null) { - return ( - - ); - } - return false; -}; - -export default AccessRedirectPage; diff --git a/src/test/mock-server/server.js b/src/test/mock-server/server.js index 9ce0a806f..44333866a 100644 --- a/src/test/mock-server/server.js +++ b/src/test/mock-server/server.js @@ -107,6 +107,8 @@ const server = mockServer(schemaWithMocks, { return result; }, getPermissionTickets: () => new MockList(6, (_, { id }) => ({ id })), + getPermissionTicketsForResource: () => + new MockList(6, (_, { id }) => ({ id })), getResourceSet: () => new MockList(8, (_, { id }) => ({ id })), myServiceAccesses: () => new MockList(8, (_, { id }) => ({ id })), mySelf: () => db.get('user'), From 940e76ddbbd15cab12b96c082c2a6cc714340034 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Fri, 18 Jun 2021 14:35:13 -0700 Subject: [PATCH 05/21] Move 90% of namespace access into 1 file. --- .../grant-access-dialog.tsx | 9 +- .../grant-access-dialog.tsx | 2 +- .../pages/manager/namespace-access/index.tsx | 169 +++++++++++++++++- 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx b/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx index 337206664..7bd48c94f 100644 --- a/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx +++ b/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx @@ -39,7 +39,7 @@ const ShareResourceDialog: React.FC = ({ prodEnvId, resource, resourceId, - queryKey + queryKey, }) => { const client = useQueryClient(); const grant = useApiMutation(mutation); @@ -114,7 +114,7 @@ const ShareResourceDialog: React.FC = ({ Permissions - {resource.resource_scopes.map((r) => ( + {resource?.resource_scopes.map((r) => ( {r.name} @@ -144,10 +144,7 @@ const ShareResourceDialog: React.FC = ({ export default ShareResourceDialog; const mutation = gql` - mutation GrantUserAccess( - $prodEnvId: ID! - $data: UMAPermissionTicketInput! - ) { + mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) { grantPermissions(prodEnvId: $prodEnvId, data: $data) { id } diff --git a/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx b/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx index 084760b9e..8c74224b8 100644 --- a/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx +++ b/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx @@ -118,7 +118,7 @@ const ShareResourceDialog: React.FC = ({ Permissions - {resource.resource_scopes.map((r) => ( + {resource?.resource_scopes.map((r) => ( {r.name} diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index 31d8c6f1d..8f6e3a500 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -1,11 +1,19 @@ import * as React from 'react'; import { gql } from 'graphql-request'; -import ResourceAccess from './resource-access'; import api, { useApi } from '@/shared/services/api'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { QueryClient } from 'react-query'; import { Query } from '@/shared/types/query.types'; import { dehydrate } from 'react-query/hydration'; +import { Box, Container, Divider, Heading } from '@chakra-ui/react'; +import GrantAccessDialog from '@/components/grant-access-dialog'; +import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; +import PageHeader from '@/components/page-header'; +import Head from 'next/head'; +import ResourcesManager from '@/components/resources-manager'; +import { useAuth } from '@/shared/services/auth'; +import EmptyPane from '@/components/empty-pane'; +import UsersAccessList from '@/components/users-access-list'; export const getServerSideProps: GetServerSideProps = async (context) => { const queryKey = 'namespaceAccess'; @@ -30,7 +38,15 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const AccessRedirectPage: React.FC< InferGetServerSidePropsType > = ({ queryKey }) => { - const { data } = useApi( + const { user } = useAuth(); + const breadcrumbs = user + ? [ + { href: '/manager/namespaces', text: 'Namespaces' }, + { href: '/manager/namespaces', text: user.namespace }, + ] + : []; + + const { data, isSuccess } = useApi( queryKey, { query }, { @@ -38,14 +54,99 @@ const AccessRedirectPage: React.FC< } ); + const resourceId = data?.currentNamespace?.id; + const prodEnvId = data?.currentNamespace?.prodEnvId; + + const permissions = useApi( + 'namespacePermissions', + { + query: permissionsQuery, + variables: { + resourceId, + prodEnvId, + }, + }, + { + enabled: Boolean(data) && isSuccess, + } + ); + const requests = permissions.data?.getPermissionTicketsForResource.filter( + (p) => !p.granted + ); + return ( - + <> + + {`API Program Services | Resources | ${permissions.data?.getResourceSet.type} ${permissions.data?.getResourceSet.name}`} + + + 0 && ( + + ) + } + breadcrumb={breadcrumbs} + title="Namespace Access" + /> + + + Users with Access + + + + {!resourceId && ( + + )} + p.granted + )} + resourceId={resourceId} + prodEnvId={prodEnvId} + queryKey={queryKey} + /> + + + + + Service Accounts with Access + + + + + + ); }; @@ -63,3 +164,53 @@ const query = gql` } } `; + +const permissionsQuery = gql` + query GetPermissions($resourceId: String!, $prodEnvId: ID!) { + getPermissionTicketsForResource( + prodEnvId: $prodEnvId + resourceId: $resourceId + ) { + id + owner + ownerName + requester + requesterName + resource + resourceName + scope + scopeName + granted + } + + getUmaPoliciesForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { + id + name + description + type + logic + decisionStrategy + owner + clients + users + scopes + } + + getResourceSet(prodEnvId: $prodEnvId, resourceId: $resourceId) { + id + name + type + resource_scopes { + name + } + } + + Environment(where: { id: $prodEnvId }) { + name + product { + id + name + } + } + } +`; From dc3d3208ba4148b308841bea2584214c4adcd4aa Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Fri, 18 Jun 2021 16:41:19 -0700 Subject: [PATCH 06/21] Remove unneeded files. --- .../inline-permissions-list.tsx | 1 - .../components/service-accounts-list/index.ts | 1 + .../service-accounts-list.tsx | 119 ++++++++++++ .../devportal/resources/service-accounts.tsx | 91 --------- .../pages/manager/namespace-access/index.tsx | 7 + .../namespace-access/resource-access.tsx | 175 ------------------ 6 files changed, 127 insertions(+), 267 deletions(-) create mode 100644 src/nextapp/components/service-accounts-list/index.ts create mode 100644 src/nextapp/components/service-accounts-list/service-accounts-list.tsx delete mode 100644 src/nextapp/pages/devportal/resources/service-accounts.tsx delete mode 100644 src/nextapp/pages/manager/namespace-access/resource-access.tsx diff --git a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx index 83bde4fd3..9454a4dc8 100644 --- a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx +++ b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx @@ -4,7 +4,6 @@ import { Tag, TagCloseButton, Wrap, WrapItem } from '@chakra-ui/react'; interface InlinePermissionsListProps { data: { id: string; - scope: string; scopeName: string; }[]; enableRevoke: boolean; diff --git a/src/nextapp/components/service-accounts-list/index.ts b/src/nextapp/components/service-accounts-list/index.ts new file mode 100644 index 000000000..1ebcf7658 --- /dev/null +++ b/src/nextapp/components/service-accounts-list/index.ts @@ -0,0 +1 @@ +export { default } from './service-accounts-list'; diff --git a/src/nextapp/components/service-accounts-list/service-accounts-list.tsx b/src/nextapp/components/service-accounts-list/service-accounts-list.tsx new file mode 100644 index 000000000..8e58e8c16 --- /dev/null +++ b/src/nextapp/components/service-accounts-list/service-accounts-list.tsx @@ -0,0 +1,119 @@ +import * as React from 'react'; +import { + Button, + Icon, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableCaption, + useToast, +} from '@chakra-ui/react'; +import { FaMinusCircle } from 'react-icons/fa'; +import { gql } from 'graphql-request'; +import InlinePermissionsList from '@/components/inline-permissions-list'; +import { useApiMutation } from '@/shared/services/api'; +import { QueryKey, useQueryClient } from 'react-query'; +import { UmaPolicy } from '@/shared/types/query.types'; + +interface ServiceAccountsListProps { + data: UmaPolicy[]; + prodEnvId: string; + resourceId: string; + queryKey: QueryKey; +} + +const ServiceAccountsList: React.FC = ({ + data, + prodEnvId, + resourceId, + queryKey, +}) => { + const toast = useToast(); + const client = useQueryClient(); + const revoke = useApiMutation(revokeMutation); + const list = data?.sort((a, b) => a.name.localeCompare(b.name)); + + const handleRevoke = async (policyId: string) => { + try { + await revoke.mutateAsync({ prodEnvId, resourceId, policyId }); + toast({ + title: 'Access Revoked', + status: 'success', + }); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + title: 'Revoke Access Scope Failed', + description: err?.message, + status: 'error', + }); + } + }; + + return ( + <> + + - + + + + + + + + + {list + ?.filter((p) => p.users == null) + .map((item) => ( + + + + + + ))} + +
SubjectPermissionActions
+ {item.clients != null + ? item.clients.join(',') + : item.users.join(',')} + + ({ id: s, scopeName: s }))} + onRevoke={() => false} + /> + + +
+ + ); +}; + +export default ServiceAccountsList; + +const revokeMutation = gql` + mutation RevokeSAAccess( + $prodEnvId: ID! + $resourceId: String! + $policyId: String! + ) { + deleteUmaPolicy( + prodEnvId: $prodEnvId + resourceId: $resourceId + policyId: $policyId + ) + } +`; diff --git a/src/nextapp/pages/devportal/resources/service-accounts.tsx b/src/nextapp/pages/devportal/resources/service-accounts.tsx deleted file mode 100644 index 031394e84..000000000 --- a/src/nextapp/pages/devportal/resources/service-accounts.tsx +++ /dev/null @@ -1,91 +0,0 @@ - -import { Button, Icon, Table, Thead, Tbody, Tr, Th, Td, TableCaption, useToast } from "@chakra-ui/react" -import { gql } from 'graphql-request'; - -import InlinePermissionsList from '@/components/inline-permissions-list'; -import { useApiMutation } from '@/shared/services/api'; -import { QueryKey, useQueryClient } from 'react-query'; -import { FaCheck, FaMinusCircle } from 'react-icons/fa'; - -interface RevokeVariables { - prodEnvId: string - resourceId: string - policyId: string - } - -function List({ prodEnvId, resourceId, data, queryKey }) { - const list = data?.sort((a,b) => a.name.localeCompare(b.name)) - - const toast = useToast(); - const client = useQueryClient(); - const revoke = useApiMutation(revokeMutation); - - const handleRevoke = async (policyId: string) => { - try { - await revoke.mutateAsync({ prodEnvId, resourceId, policyId }); - toast({ - title: 'Access Revoked', - status: 'success', - }); - client.invalidateQueries(queryKey); - } catch (err) { - toast({ - title: 'Revoke Access Scope Failed', - description: err?.message, - status: 'error', - }); - } - }; - return ( - <> - - - - - - - - - - - - {list?.filter(p => p.users == null).map((item, index) => ( - - - - - - ))} - -
SubjectPermissionActions
{item.clients != null ? item.clients.join(',') : item.users.join(',')} - ({ id: s, scopeName: s}))} - onRevoke={()=>false} - /> - - -
- - ) - } - -const revokeMutation = gql` - mutation RevokeSAAccess( - $prodEnvId: ID!, - $resourceId: String!, - $policyId: String!) { - deleteUmaPolicy(prodEnvId: $prodEnvId, resourceId: $resourceId, policyId: $policyId) -} -` - -export default List \ No newline at end of file diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index 8f6e3a500..3c4631f09 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -14,6 +14,7 @@ import ResourcesManager from '@/components/resources-manager'; import { useAuth } from '@/shared/services/auth'; import EmptyPane from '@/components/empty-pane'; import UsersAccessList from '@/components/users-access-list'; +import ServiceAccountsList from '@/components/service-accounts-list'; export const getServerSideProps: GetServerSideProps = async (context) => { const queryKey = 'namespaceAccess'; @@ -144,6 +145,12 @@ const AccessRedirectPage: React.FC< /> + diff --git a/src/nextapp/pages/manager/namespace-access/resource-access.tsx b/src/nextapp/pages/manager/namespace-access/resource-access.tsx deleted file mode 100644 index a9cce684f..000000000 --- a/src/nextapp/pages/manager/namespace-access/resource-access.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import * as React from 'react'; -import api, { useApi } from '@/shared/services/api'; -import { Box, Container, Divider, Heading, Text } from '@chakra-ui/react'; -import EmptyPane from '@/components/empty-pane'; -import GrantAccessDialog from '@/components/grant-access-dialog'; -import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; -import Head from 'next/head'; -import PageHeader from '@/components/page-header'; -import ResourcesManager from '@/components/resources-manager'; -import UsersAccessList from '@/components/users-access-list'; -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import { QueryClient } from 'react-query'; -import { getSession } from '@/shared/services/auth'; -import { Query } from '@/shared/types/query.types'; -import { dehydrate } from 'react-query/hydration'; -import { gql } from 'graphql-request'; - -import ServiceAccounts from '../../devportal/resources/service-accounts'; - -import breadcrumbs from '@/components/ns-breadcrumb'; - -interface AccessResourceProps { - queryKey: string; - variables?: { prodEnvId: string; resourceId: string }; -} - -const ApiAccessResourcePage: React.FC = ({ - queryKey, - variables, -}) => { - const { data } = useApi(queryKey, { query, variables }, { suspense: false }); - if (!data) { - return <>; - } - const { prodEnvId, resourceId } = variables; - const requests = data.getPermissionTicketsForResource?.filter( - (p) => !p.granted - ); - - const resource = data.getResourceSet; - - // title={`${resource.type} ${resource.name}`} - - return ( - <> - - {`API Program Services | Resources | Name`} - - - 0 && ( - - ) - } - breadcrumb={breadcrumbs()} - title="Namespace Access" - /> - - - Users with Access - - - - {!resourceId && ( - - )} - p.granted - )} - resourceId={resourceId} - prodEnvId={prodEnvId} - queryKey={queryKey} - /> - - - - - Service Accounts with Access - - - - - - - - ); -}; - -export default ApiAccessResourcePage; - -const query = gql` - query GetPermissions($resourceId: String!, $prodEnvId: ID!) { - getPermissionTicketsForResource( - prodEnvId: $prodEnvId - resourceId: $resourceId - ) { - id - owner - ownerName - requester - requesterName - resource - resourceName - scope - scopeName - granted - } - - getUmaPoliciesForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { - id - name - description - type - logic - decisionStrategy - owner - clients - users - scopes - } - - getResourceSet(prodEnvId: $prodEnvId, resourceId: $resourceId) { - id - name - type - resource_scopes { - name - } - } - - Environment(where: { id: $prodEnvId }) { - name - product { - id - name - } - } - } -`; From 8ecd51e9f95ea7638ee7f178450022c403775dba Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Fri, 2 Jul 2021 13:59:41 -0700 Subject: [PATCH 07/21] Fix type error --- .../pages/devportal/resources/[id].tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/nextapp/pages/devportal/resources/[id].tsx b/src/nextapp/pages/devportal/resources/[id].tsx index 5f1de171c..c3d904203 100644 --- a/src/nextapp/pages/devportal/resources/[id].tsx +++ b/src/nextapp/pages/devportal/resources/[id].tsx @@ -15,7 +15,7 @@ import { Query } from '@/shared/types/query.types'; import { dehydrate } from 'react-query/hydration'; import { gql } from 'graphql-request'; -import ServiceAccounts from './service-accounts' +import ServiceAccounts from '@/components/service-accounts-list'; export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; @@ -50,9 +50,11 @@ const ApiAccessResourcePage: React.FC< > = ({ queryKey, variables }) => { const { data } = useApi(queryKey, { query, variables }, { suspense: false }); const { prodEnvId, resourceId } = variables; - const requests = data.getPermissionTicketsForResource?.filter((p) => !p.granted); + const requests = data.getPermissionTicketsForResource?.filter( + (p) => !p.granted + ); - const resource = data.getResourceSet + const resource = data.getResourceSet; return ( <> @@ -73,7 +75,10 @@ const ApiAccessResourcePage: React.FC< } breadcrumb={[ { href: '/devportal/access', text: 'API Access' }, - { href: '/devportal/access/' + data?.Environment?.product.id, text: `${data?.Environment?.product.name} Resources` }, + { + href: '/devportal/access/' + data?.Environment?.product.id, + text: `${data?.Environment?.product.name} Resources`, + }, ]} title={`${resource.type} ${resource.name}`} /> @@ -101,12 +106,13 @@ const ApiAccessResourcePage: React.FC< )} p.granted)} + data={data?.getPermissionTicketsForResource.filter( + (p) => p.granted + )} resourceId={resourceId} prodEnvId={prodEnvId} queryKey={queryKey} /> - @@ -125,7 +131,12 @@ const ApiAccessResourcePage: React.FC< /> - + @@ -136,7 +147,10 @@ export default ApiAccessResourcePage; const query = gql` query GetPermissions($resourceId: String!, $prodEnvId: ID!) { - getPermissionTicketsForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { + getPermissionTicketsForResource( + prodEnvId: $prodEnvId + resourceId: $resourceId + ) { id owner ownerName @@ -170,7 +184,7 @@ const query = gql` name } } - + Environment(where: { id: $prodEnvId }) { name product { From 33367d08243b98e1667c4fd10943b4717814ec41 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Fri, 2 Jul 2021 14:00:26 -0700 Subject: [PATCH 08/21] Fix lint error. --- src/nextapp/pages/devportal/resources/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/pages/devportal/resources/[id].tsx b/src/nextapp/pages/devportal/resources/[id].tsx index c3d904203..fbd65d505 100644 --- a/src/nextapp/pages/devportal/resources/[id].tsx +++ b/src/nextapp/pages/devportal/resources/[id].tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import api, { useApi } from '@/shared/services/api'; -import { Box, Container, Divider, Heading, Text } from '@chakra-ui/react'; +import { Box, Container, Divider, Heading } from '@chakra-ui/react'; import EmptyPane from '@/components/empty-pane'; import GrantAccessDialog from '@/components/grant-access-dialog'; import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; From 6fa69e4ff800e03aed2adc729e7823ce5e04c15d Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 19:50:46 -0700 Subject: [PATCH 09/21] add support for consumer scope mgmt --- src/authz/matrix.csv | 287 +++++++++--------- src/authz/whitelist.json | 30 ++ src/lists/extensions/Common.ts | 7 +- .../extensions/ConsumerScopesAndRoles.ts | 103 ++++++- .../components/consumer-acl/consumer-acl.tsx | 110 ++----- .../consumer-authz/consumer-authz.tsx | 159 ++++++++++ .../components/consumer-authz/index.ts | 1 + .../components/consumer-authz/types/acl.tsx | 84 +++++ .../types}/roles.tsx | 5 +- .../consumer-authz/types/scopes.tsx | 163 ++++++++++ .../consumer-permissions.tsx | 73 ----- .../components/consumer-permissions/index.ts | 1 - .../pages/devportal/requests/new/[id].tsx | 4 +- src/nextapp/pages/manager/consumers/[id].tsx | 65 ++-- src/nextapp/pages/manager/requests/[id].tsx | 4 +- src/nextapp/shared/types/query.types.ts | 9 + .../keycloak/client-registration-service.ts | 21 +- src/services/keycloak/client-service.ts | 11 +- src/services/keystone/types.ts | 9 + src/services/workflow/types.ts | 17 +- 20 files changed, 801 insertions(+), 362 deletions(-) create mode 100644 src/nextapp/components/consumer-authz/consumer-authz.tsx create mode 100644 src/nextapp/components/consumer-authz/index.ts create mode 100644 src/nextapp/components/consumer-authz/types/acl.tsx rename src/nextapp/components/{consumer-permissions => consumer-authz/types}/roles.tsx (96%) create mode 100644 src/nextapp/components/consumer-authz/types/scopes.tsx delete mode 100644 src/nextapp/components/consumer-permissions/consumer-permissions.tsx delete mode 100644 src/nextapp/components/consumer-permissions/index.ts diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index 18a3a520f..5afe27d86 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -1,144 +1,145 @@ -ID,matchOneOfBaseQueryName,matchQueryName,matchListKey,matchOperation,matchOneOfOperation,matchOneOfFieldKey,matchNotOneOfFieldKey,matchUserNS,inRole,matchOneOfScope,matchOneOfRole,result,filters -,,,,,"update,create,delete",,,,aps-admin,,,allow, -API Owner Role Rules,,,AccessRequest,update,,isApproved,,,access-manager,,,allow, -API Owner Role Rules,,,AccessRequest,,"create,read",isApproved,,,api-owner,,,allow, -API Owner Role Rules,,,AccessRequest,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,AccessRequest,,"create,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Activity,read,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,,Alert,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Alert,,"create,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Application,,"read,update,delete",,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Application,,create,,,,,,"api-owner,provider-user",allow, -,,BusinessProfile,,,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Content,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,CredentialIssuer,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,CredentialIssuer,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,CredentialIssuer,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Dataset,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Environment,create,,active,,,api-owner,,,deny, -API Owner Role Rules,,,Environment,,"update,delete,read",active,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Environment,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Environment,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Environment,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayConsumer,,"read,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayConsumer,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayGroup,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayGroup,,"update,delete,create",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayPlugin,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,GatewayRoute,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,GatewayService,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Group,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Group,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Group,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Legal,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Legal,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Metric,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Namespace,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Namespace,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Namespace,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Organization,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,OrganizationUnit,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Product,update,,namespace,,,,,,deny, -API Owner Role Rules,,,Product,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Product,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Product,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,TemporaryIdentity,read,,,,,,,"api-owner,provider-user",allow,filterByTemporaryIdentity -API Owner Role Rules,,,User,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,getGatewayConsumerPlugins,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteGatewayConsumerPlugin,,,,,,,api-owner,,,allow, -API Owner Role Rules,,createGatewayConsumerPlugin,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getPermissionTicketsForResource,,,,,,,api-owner,,,allow, -API Owner Role Rules,,allPermissionTickets,,,,,,,api-owner,,,allow, -API Owner Role Rules,,updateConsumerGroupMembership,,,,,,,api-owner,,,allow,filterByPackageNS -API Owner Role Rules,,consumerScopesAndRoles,,,,,,,api-owner,,,allow,filterByPackageNS -API Owner Role Rules,,linkConsumerToNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,updateConsumerRoleAssignment,,,,,,,api-owner,,,allow,filterByPackageNS -Portal User Namespaces,,allNamespaces,,,,,,,portal-user,,,allow, -API Owner Role Rules,,createNamespace,,,,,,,idir-user,,,allow, -API Owner Role Rules,,deleteNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,currentNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, -API Owner Role Rules,,allResourceSets,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getUmaPoliciesForResource,,,,,,,api-owner,,,allow, -API Owner Role Rules,,createUmaPolicy,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteUmaPolicy,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getServiceAccounts,,,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,createServiceAccount,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteServiceAccount,,,,,,,api-owner,,,allow, -Developer Role Rules,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules,,myApplications,,,,,,,portal-user,,,allow,filterByOwner -Developer Role Rules,,allDiscoverableProducts,,,,,,,portal-user,,,allow,filterByActiveEnvironment -Developer Role Rules,,allDiscoverableContents,,,,,,,portal-user,,,allow, -Developer Role Rules,,DiscoverableProduct,,,,,,,portal-user,,,allow, -Developer Role Rules,,,Product,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Environment,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Dataset,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Organization,read,,,,,portal-user,,,allow, -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,,,Legal,read,,,,,portal-user,,,allow, -Developer Role Rules,,,TemporaryIdentity,read,,,,,portal-user,,,allow,filterByTemporaryIdentity -Developer Role Rules,,,User,read,,,,,portal-user,,,allow,filterBySelf -Developer Role Rules,,,CredentialIssuer,read,,,,,portal-user,,,allow, -Developer Role Rules,,,AccessRequest,,create,,,,portal-user,,,allow, -Developer Role Rules,,,AccessRequest,,"update,read",,,,portal-user,,,allow,filterByRequestor -Developer Role Rules,,,Application,create,,,,,portal-user,,,allow, -Developer Role Rules,,,Application,,"read,delete,update",,,,portal-user,,,allow,filterByOwner -Developer Role Rules,,,ServiceAccess,delete,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules,,,ServiceAccess,read,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules - Fields,,,Product,read,,"id,name,dataset,organization,organizationUnit,environments",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Dataset,read,,,"isInCatalog,extSource,extForeignKey,extRecordHash",,portal-user,,,allow, -Developer Role Rules - Fields,,,Organization,read,,title,,,portal-user,,,allow, -Developer Role Rules - Fields,,,OrganizationUnit,read,,title,,,portal-user,,,allow, -Developer Role Rules - Fields,,,GatewayService,read,,"name,routes",,,portal-user,,,allow, -Developer Role Rules - Fields,,,GatewayRoute,read,,"name,methods,hosts,paths",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Legal,read,,"title,description,link",,,portal-user,,,allow, -Developer Role Rules - Fields,,,TemporaryIdentity,read,,"id,userId,name,email,username",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Content,read,,"slug,title,description",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Application,create,,"name,description",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Application,read,,"appId,name,owner",,,portal-user,,,allow, -Developer Role Rules - Fields,,,User,read,,"name,username,email,legalsAgreed",,,portal-user,,,allow, -Developer Role Rules - Fields,,,ServiceAccess,read,,"name,active,productEnvironment",,,portal-user,,,allow, -Developer Role Rules - Fields,,,CredentialIssuer,read,,"id,name,flow,resourceType",,,portal-user,,,allow, -Developer Role Rules - Fields,,,AccessRequest,create,,"name,controls,requestor,application,productEnvironment",,,portal-user,,,allow, -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, -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, -Portal User,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner -Portal User,,myApplications,,,,,,,portal-user,,,allow,filterByOwner -API Owner Role Rules,,allGatewayServicesByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allProductsByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allAccessRequestsByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentPackageNS -API Owner Role Rules,,allServiceAccessesByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentProductNSOrNS -API Owner Role Rules,,allNamespaceServiceAccounts,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allCredentialIssuersByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -,,getUmaPoliciesByResourceName,,,,,,,api-owner,,,allow, -,,getResourceOwners,,,,,,,api-owner,,,allow, -Portal User or Guest,,allDiscoverableContents,,,,,,,,,"portal-user,guest",allow,filterByNamespaceOrPublic -Portal User or Guest,,allDiscoverableProducts,,,,,,,,,"portal-user,guest",allow,filterByActiveEnvironment -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,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,,,"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, -ALL USERS,,mySelf,,,,,,,,,,allow,filterBySelf -ALL USERS,,acceptLegal,,,,,,,,,,allow, +ID,matchOneOfBaseQueryName,matchQueryName,matchListKey,matchOperation,matchOneOfOperation,matchOneOfFieldKey,matchNotOneOfFieldKey,matchUserNS,inRole,matchOneOfScope,matchOneOfRole,result,filters +,,,,,"update,create,delete",,,,aps-admin,,,allow, +API Owner Role Rules,,,AccessRequest,update,,isApproved,,,access-manager,,,allow, +API Owner Role Rules,,,AccessRequest,,"create,read",isApproved,,,api-owner,,,allow, +API Owner Role Rules,,,AccessRequest,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,AccessRequest,,"create,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Activity,read,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,,Alert,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Alert,,"create,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Application,,"read,update,delete",,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Application,,create,,,,,,"api-owner,provider-user",allow, +,,BusinessProfile,,,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Content,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,CredentialIssuer,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,CredentialIssuer,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,CredentialIssuer,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Dataset,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Environment,create,,active,,,api-owner,,,deny, +API Owner Role Rules,,,Environment,,"update,delete,read",active,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Environment,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Environment,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Environment,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayConsumer,,"read,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayConsumer,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayGroup,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayGroup,,"update,delete,create",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayPlugin,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,GatewayRoute,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,GatewayService,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Group,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Group,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Group,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Legal,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Legal,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Metric,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Namespace,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Namespace,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Namespace,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Organization,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,OrganizationUnit,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Product,update,,namespace,,,,,,deny, +API Owner Role Rules,,,Product,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Product,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Product,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,TemporaryIdentity,read,,,,,,,"api-owner,provider-user",allow,filterByTemporaryIdentity +API Owner Role Rules,,,User,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,getGatewayConsumerPlugins,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,createGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getPermissionTicketsForResource,,,,,,,api-owner,,,allow, +API Owner Role Rules,,allPermissionTickets,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateConsumerGroupMembership,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,consumerScopesAndRoles,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,linkConsumerToNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateConsumerRoleAssignment,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,updateConsumerScopeAssignment,,,,,,,api-owner,,,allow,filterByPackageNS +Portal User Namespaces,,allNamespaces,,,,,,,portal-user,,,allow, +API Owner Role Rules,,createNamespace,,,,,,,idir-user,,,allow, +API Owner Role Rules,,deleteNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,currentNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, +API Owner Role Rules,,allResourceSets,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getUmaPoliciesForResource,,,,,,,api-owner,,,allow, +API Owner Role Rules,,createUmaPolicy,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteUmaPolicy,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getServiceAccounts,,,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,createServiceAccount,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteServiceAccount,,,,,,,api-owner,,,allow, +Developer Role Rules,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules,,myApplications,,,,,,,portal-user,,,allow,filterByOwner +Developer Role Rules,,allDiscoverableProducts,,,,,,,portal-user,,,allow,filterByActiveEnvironment +Developer Role Rules,,allDiscoverableContents,,,,,,,portal-user,,,allow, +Developer Role Rules,,DiscoverableProduct,,,,,,,portal-user,,,allow, +Developer Role Rules,,,Product,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Environment,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Dataset,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Organization,read,,,,,portal-user,,,allow, +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,,,Legal,read,,,,,portal-user,,,allow, +Developer Role Rules,,,TemporaryIdentity,read,,,,,portal-user,,,allow,filterByTemporaryIdentity +Developer Role Rules,,,User,read,,,,,portal-user,,,allow,filterBySelf +Developer Role Rules,,,CredentialIssuer,read,,,,,portal-user,,,allow, +Developer Role Rules,,,AccessRequest,,create,,,,portal-user,,,allow, +Developer Role Rules,,,AccessRequest,,"update,read",,,,portal-user,,,allow,filterByRequestor +Developer Role Rules,,,Application,create,,,,,portal-user,,,allow, +Developer Role Rules,,,Application,,"read,delete,update",,,,portal-user,,,allow,filterByOwner +Developer Role Rules,,,ServiceAccess,delete,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules,,,ServiceAccess,read,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules - Fields,,,Product,read,,"id,name,dataset,organization,organizationUnit,environments",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Dataset,read,,,"isInCatalog,extSource,extForeignKey,extRecordHash",,portal-user,,,allow, +Developer Role Rules - Fields,,,Organization,read,,title,,,portal-user,,,allow, +Developer Role Rules - Fields,,,OrganizationUnit,read,,title,,,portal-user,,,allow, +Developer Role Rules - Fields,,,GatewayService,read,,"name,routes",,,portal-user,,,allow, +Developer Role Rules - Fields,,,GatewayRoute,read,,"name,methods,hosts,paths",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Legal,read,,"title,description,link",,,portal-user,,,allow, +Developer Role Rules - Fields,,,TemporaryIdentity,read,,"id,userId,name,email,username",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Content,read,,"slug,title,description",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Application,create,,"name,description",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Application,read,,"appId,name,owner",,,portal-user,,,allow, +Developer Role Rules - Fields,,,User,read,,"name,username,email,legalsAgreed",,,portal-user,,,allow, +Developer Role Rules - Fields,,,ServiceAccess,read,,"name,active,productEnvironment",,,portal-user,,,allow, +Developer Role Rules - Fields,,,CredentialIssuer,read,,"id,name,flow,resourceType",,,portal-user,,,allow, +Developer Role Rules - Fields,,,AccessRequest,create,,"name,controls,requestor,application,productEnvironment",,,portal-user,,,allow, +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, +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, +Portal User,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner +Portal User,,myApplications,,,,,,,portal-user,,,allow,filterByOwner +API Owner Role Rules,,allGatewayServicesByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allProductsByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allAccessRequestsByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentPackageNS +API Owner Role Rules,,allServiceAccessesByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentProductNSOrNS +API Owner Role Rules,,allNamespaceServiceAccounts,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allCredentialIssuersByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +,,getUmaPoliciesByResourceName,,,,,,,api-owner,,,allow, +,,getResourceOwners,,,,,,,api-owner,,,allow, +Portal User or Guest,,allDiscoverableContents,,,,,,,,,"portal-user,guest",allow,filterByNamespaceOrPublic +Portal User or Guest,,allDiscoverableProducts,,,,,,,,,"portal-user,guest",allow,filterByActiveEnvironment +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,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,,,"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, +ALL USERS,,mySelf,,,,,,,,,,allow,filterBySelf +ALL USERS,,acceptLegal,,,,,,,,,,allow, ALL USERS,,,User,update,,,,,,,,allow, \ No newline at end of file diff --git a/src/authz/whitelist.json b/src/authz/whitelist.json index ce93460d5..dbc06ec2b 100644 --- a/src/authz/whitelist.json +++ b/src/authz/whitelist.json @@ -746,5 +746,35 @@ "referer": "http://localhost:4180/manager/requests/60dba098fb76a41bf6a8e74e", "query": "\n query GetAccessRequest($id: ID!, $rid: String!) {\n AccessRequest(where: { id: $id }) {\n id\n name\n isApproved\n isIssued\n controls\n additionalDetails\n createdAt\n requestor {\n name\n username\n email\n }\n application {\n name\n }\n serviceAccess {\n id\n }\n productEnvironment {\n name\n additionalDetailsToRequest\n product {\n name\n }\n credentialIssuer {\n availableScopes\n clientRoles\n }\n }\n }\n\n allActivities(sortBy: createdAt_DESC, where: { refId: $rid }) {\n id\n type\n name\n action\n result\n message\n context\n refId\n namespace\n extRefId\n createdAt\n actor {\n name\n username\n }\n }\n }\n", "added": "2021-06-29T22:48:41.365Z" + }, + "78b1987847e9566263124c3b003f06bd": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n mutation ToggleConsumerScopes(\n $prodEnvId: ID!\n $consumerUsername: String!\n $scopeName: String!\n $grant: Boolean!\n ) {\n updateConsumerScopeAssignment(\n prodEnvId: $prodEnvId\n consumerUsername: $consumerUsername\n scopeName: $scopeName\n grant: $grant\n )\n }\n", + "added": "2021-07-02T23:59:23.643Z" + }, + "7238409bbbc0954b2c00cd03a8162054": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T00:43:58.265Z" + }, + "1aa9f1d913d81a275515d46091053bec": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n application {\n name\n owner {\n username\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:43:27.940Z" + }, + "13758b33b8f2db459bfb37fd789e5309": { + "referer": "http://localhost:4180/manager/consumers/60b5bff29dc26943fb23d4b6", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n username\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:46:50.029Z" + }, + "0d9d51ac4ff398172f53dac66b86e481": { + "referer": "http://localhost:4180/manager/consumers/60ad8e1044cf9a36ef674085", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n name\n username\n email\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:47:50.661Z" + }, + "b35cd8088be328bd3528db32a3ba97cf": { + "referer": "http://localhost:4180/manager/consumers/60ad8e1044cf9a36ef674085", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n name\n username\n email\n }\n }\n }\n\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:47:59.254Z" } } \ No newline at end of file diff --git a/src/lists/extensions/Common.ts b/src/lists/extensions/Common.ts index 0f0dbf899..4341be54e 100644 --- a/src/lists/extensions/Common.ts +++ b/src/lists/extensions/Common.ts @@ -7,6 +7,7 @@ import { import { IssuerEnvironmentConfig, getIssuerEnvironmentConfig, + checkIssuerEnvironmentConfig, } from '../../services/workflow/types'; import { UMAPermissionService, @@ -60,11 +61,15 @@ export async function getEnvironmentContext( access ).where ); - const issuerEnvConfig: IssuerEnvironmentConfig = getIssuerEnvironmentConfig( + const issuerEnvConfig: IssuerEnvironmentConfig = checkIssuerEnvironmentConfig( prodEnv.credentialIssuer, prodEnv.name ); + if (issuerEnvConfig == null) { + return null; + } + const openid = await getOpenidFromIssuer(issuerEnvConfig.issuerUrl); const subjectToken = context.req.headers['x-forwarded-access-token']; const subjectUuid = context.req.user.sub; diff --git a/src/lists/extensions/ConsumerScopesAndRoles.ts b/src/lists/extensions/ConsumerScopesAndRoles.ts index 658536cd1..ebdf92336 100644 --- a/src/lists/extensions/ConsumerScopesAndRoles.ts +++ b/src/lists/extensions/ConsumerScopesAndRoles.ts @@ -8,6 +8,7 @@ import { KongConsumerService } from '../../services/kong'; import { KeycloakUserService, KeycloakClientService, + KeycloakClientRegistrationService, } from '../../services/keycloak'; import { EnvironmentWhereInput } from '@/services/keystone/types'; import { mergeWhereClause } from '@keystonejs/utils'; @@ -72,6 +73,19 @@ module.exports = { args.prodEnvId, access ); + if (envCtx == null) { + logger.warn( + 'Credential Issuer did not have an environment for prodenv %s', + args.prodEnvId + ); + return { + id: '', + consumerType: '', + defaultScopes: [], + optionalScopes: [], + clientRoles: [], + } as any; + } try { const kcClientService = new KeycloakClientService( envCtx.issuerEnvConfig.issuerUrl @@ -107,10 +121,14 @@ module.exports = { userId, client.id ); + const defaultScopes = await kcClientService.listDefaultScopes( + consumerClient.id + ); + return { id: userId, consumerType: 'client', - defaultScopes: [], + defaultScopes: defaultScopes.map((v: any) => v.name), optionalScopes: [], clientRoles: userRoles.map((r: any) => r.name), } as any; @@ -259,6 +277,89 @@ module.exports = { }, access: EnforcementPoint, }, + { + schema: + 'updateConsumerScopeAssignment( prodEnvId: ID!, consumerUsername: String!, scopeName: String!, grant: Boolean! ): Boolean', + resolver: async ( + item: any, + args: any, + context: any, + info: any, + { query, access }: any + ) => { + const envCtx = await getEnvironmentContext( + context, + args.prodEnvId, + access + ); + try { + const kcClientService = new KeycloakClientService( + envCtx.issuerEnvConfig.issuerUrl + ); + const kcClientRegService = new KeycloakClientRegistrationService( + envCtx.issuerEnvConfig.issuerUrl, + null + ); + await kcClientService.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + await kcClientRegService.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + + const client = await kcClientService.findByClientId( + envCtx.issuerEnvConfig.clientId + ); + + const availableScopes = await kcClientService.listDefaultScopes( + client.id + ); + + const selectedScope = availableScopes + .filter((r: any) => r.name === args.scopeName) + .map((r: any) => r.id); + + assert.strictEqual( + selectedScope.length, + 1, + 'Scope not found in IdP' + ); + + logger.debug( + '[updateConsumerScopeAssignment] selected %j', + selectedScope + ); + + const isClient = await kcClientService.isClient( + args.consumerUsername + ); + + assert.strictEqual( + isClient, + true, + 'Only clients support scopes' + ); + + await kcClientRegService.syncClientScopes( + args.consumerUsername, + client.id, + args.grant ? selectedScope : [], + args.grant ? [] : selectedScope + ); + } catch (err) { + logger.error( + '[updateConsumerScopeAssignment] Failed to update %s', + err + ); + throw err; + } + + return args.grant; + }, + access: EnforcementPoint, + }, ], }); }, diff --git a/src/nextapp/components/consumer-acl/consumer-acl.tsx b/src/nextapp/components/consumer-acl/consumer-acl.tsx index d6937a4c3..ae132dc2c 100644 --- a/src/nextapp/components/consumer-acl/consumer-acl.tsx +++ b/src/nextapp/components/consumer-acl/consumer-acl.tsx @@ -1,116 +1,40 @@ import * as React from 'react'; import { Product, Environment } from '@/shared/types/query.types'; -import { Box, Divider, Text, Switch, useToast } from '@chakra-ui/react'; -import { gql } from 'graphql-request'; -import { QueryKey, useQueryClient } from 'react-query'; -import { useApiMutation } from '@/shared/services/api'; +import { Box, Divider, Heading, Switch } from '@chakra-ui/react'; interface ConsumerACLProps { - queryKey: any[]; - consumerId: string; aclGroups: string[]; products: Product[]; } -const ConsumerACL: React.FC = ({ - queryKey, - consumerId, - aclGroups, - products, -}) => { - const client = useQueryClient(); - const grantMutation = useApiMutation(mutation); - const toast = useToast(); - const handleGrantToggle = React.useCallback( - (prodEnvId: string, group: string) => async ( - event: React.ChangeEvent - ) => { - event.preventDefault(); - event.stopPropagation(); - - const grant = event.target.checked; - - try { - await grantMutation.mutateAsync({ - prodEnvId, - group, - consumerId, - grant, - }); - client.invalidateQueries(queryKey); - toast({ - title: 'ACL Updated', - status: 'success', - }); - } catch (err) { - toast({ - title: 'ACL update failed', - description: err?.message, - status: 'error', - }); - } - }, - [client, queryKey, grantMutation, toast] - ); - +const ConsumerACL: React.FC = ({ aclGroups, products }) => { const productGroups = [].concat .apply( [], products.map((product) => product.environments) ) .map((env: Environment) => env.appId); - return ( - - {products.map((product: Product) => { - return product.environments.map((env: Environment) => ( - - {' '} - {product.name}{' '} - - {env.name} - - - )); - })} + const readonlyGroups = aclGroups.filter( + (group: string) => !productGroups.includes(group) + ); + return readonlyGroups.length > 0 ? ( + <> + + Legacy ACL Groups + - {aclGroups - .filter((group: string) => !productGroups.includes(group)) - .map((group: string) => ( + + + {readonlyGroups.map((group: string) => ( {group} ))} - + + + ) : ( + <> ); }; export default ConsumerACL; - -const mutation = gql` - mutation ToggleConsumerACLMembership( - $prodEnvId: ID! - $consumerId: ID! - $group: String! - $grant: Boolean! - ) { - updateConsumerGroupMembership( - prodEnvId: $prodEnvId - consumerId: $consumerId - group: $group - grant: $grant - ) - } -`; diff --git a/src/nextapp/components/consumer-authz/consumer-authz.tsx b/src/nextapp/components/consumer-authz/consumer-authz.tsx new file mode 100644 index 000000000..225d4a31f --- /dev/null +++ b/src/nextapp/components/consumer-authz/consumer-authz.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { Product, Environment } from '@/shared/types/query.types'; +import { + Box, + Divider, + Icon, + Heading, + Text, + Switch, + useToast, +} from '@chakra-ui/react'; +import { gql } from 'graphql-request'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; +import { FaPlusCircle, FaFolder, FaFolderOpen } from 'react-icons/fa'; + +import ACLComponent from './types/acl'; +import RolesComponent from './types/roles'; +import ScopesComponent from './types/scopes'; + +interface ConsumerACLProps { + queryKey?: any[]; + consumerId: string; + consumerUsername: string; + consumerAclGroups: string[]; + aclGroups?: string[]; + products: Product[]; +} + +const ConsumerAuthz: React.FC = ({ + queryKey, + products, + consumerId, + consumerUsername, + consumerAclGroups, +}) => { + return ( + <> + + Authorization + + + + {products + .filter((p) => p.environments.length > 0) + .filter( + (p) => p.environments.filter((e) => e.flow != 'public').length > 0 + ) + .map((d) => ( + + + + + 0 ? FaFolder : FaFolderOpen} + color={ + d.environments.length > 0 ? 'bc-blue-alt' : 'gray.200' + } + mr={4} + boxSize="1.5rem" + /> + + {d.name} + + + + + + {d.environments.map((e, index: number, arr: any) => ( + + + + + {e.name} + + + + {e.flow === 'client-credentials' && ( + + + + )} + {e.flow === 'client-credentials' && ( + + + + )} + {(e.flow === 'kong-api-key-acl' || + e.flow == 'kong-acl-only') && ( + + + + )} + + ))} + + + + + ))} + + + + ); +}; + +export default ConsumerAuthz; diff --git a/src/nextapp/components/consumer-authz/index.ts b/src/nextapp/components/consumer-authz/index.ts new file mode 100644 index 000000000..cefe3e74c --- /dev/null +++ b/src/nextapp/components/consumer-authz/index.ts @@ -0,0 +1 @@ +export { default } from './consumer-authz'; diff --git a/src/nextapp/components/consumer-authz/types/acl.tsx b/src/nextapp/components/consumer-authz/types/acl.tsx new file mode 100644 index 000000000..6b6eade96 --- /dev/null +++ b/src/nextapp/components/consumer-authz/types/acl.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { Product, Environment } from '@/shared/types/query.types'; +import { Box, Divider, Text, Switch, useToast } from '@chakra-ui/react'; +import { gql } from 'graphql-request'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; + +interface ConsumerACLProps { + queryKey: any[]; + consumerId: string; + aclGroups: string[]; + env: Environment; +} + +const ConsumerACL: React.FC = ({ + queryKey, + consumerId, + aclGroups, + env, +}) => { + const client = useQueryClient(); + const grantMutation = useApiMutation(mutation); + const toast = useToast(); + const handleGrantToggle = React.useCallback( + (prodEnvId: string, group: string) => async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + event.stopPropagation(); + + const grant = event.target.checked; + + try { + await grantMutation.mutateAsync({ + prodEnvId, + group, + consumerId, + grant, + }); + client.invalidateQueries(queryKey); + toast({ + title: 'ACL Updated', + status: 'success', + }); + } catch (err) { + toast({ + title: 'ACL update failed', + description: err?.message, + status: 'error', + }); + } + }, + [client, queryKey, grantMutation, toast] + ); + + return ( + + + + ); +}; + +export default ConsumerACL; + +const mutation = gql` + mutation ToggleConsumerACLMembership( + $prodEnvId: ID! + $consumerId: ID! + $group: String! + $grant: Boolean! + ) { + updateConsumerGroupMembership( + prodEnvId: $prodEnvId + consumerId: $consumerId + group: $group + grant: $grant + ) + } +`; diff --git a/src/nextapp/components/consumer-permissions/roles.tsx b/src/nextapp/components/consumer-authz/types/roles.tsx similarity index 96% rename from src/nextapp/components/consumer-permissions/roles.tsx rename to src/nextapp/components/consumer-authz/types/roles.tsx index 016beeb38..0195509ef 100644 --- a/src/nextapp/components/consumer-permissions/roles.tsx +++ b/src/nextapp/components/consumer-authz/types/roles.tsx @@ -43,7 +43,7 @@ const RolesComponent: React.FC = ({ prodEnvId, credentialIssuer, }) => { - const queryKey = ['consumer-scopes-roles', prodEnvId, consumerUsername]; + const queryKey = ['consumer-roles', prodEnvId, consumerUsername]; const variables = { prodEnvId, consumerUsername }; const { data, isFetching, isLoading, isSuccess } = useApi( queryKey, @@ -103,6 +103,9 @@ const RolesComponent: React.FC = ({ if (data == null) { return Error; } + if (data.consumerScopesAndRoles.id == '') { + return <>; + } const clientRoles = credentialIssuer.clientRoles ? JSON.parse(credentialIssuer.clientRoles) : []; diff --git a/src/nextapp/components/consumer-authz/types/scopes.tsx b/src/nextapp/components/consumer-authz/types/scopes.tsx new file mode 100644 index 000000000..cf0a2e24b --- /dev/null +++ b/src/nextapp/components/consumer-authz/types/scopes.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import { useApi } from '@/shared/services/api'; +import { gql } from 'graphql-request'; +import { CredentialIssuer } from '@/shared/types/query.types'; +import { + Button, + Checkbox, + CheckboxGroup, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + ModalHeader, + Input, + ButtonGroup, + FormControl, + FormLabel, + Icon, + useDisclosure, + VStack, + Progress, + useToast, + Box, + Text, + WrapItem, + Wrap, + useBoolean, +} from '@chakra-ui/react'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; + +interface ScopesProps { + prodEnvId: string; + consumerId: string; + consumerUsername: string; + credentialIssuer: CredentialIssuer; +} + +const ScopesComponent: React.FC = ({ + consumerId, + consumerUsername, + prodEnvId, + credentialIssuer, +}) => { + const queryKey = ['consumer-scopes', prodEnvId, consumerUsername]; + const variables = { prodEnvId, consumerUsername }; + const { data, isFetching, isLoading, isSuccess } = useApi( + queryKey, + { + query, + variables, + }, + { + retry: false, + suspense: false, + } + ); + + const [busy, setBusy] = useBoolean(false); + + const client = useQueryClient(); + const grantMutation = useApiMutation(mutation); + const toast = useToast(); + const handleGrantToggle = React.useCallback( + (scopeName: string) => async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + event.stopPropagation(); + + const grant = event.target.checked; + + try { + setBusy.on(); + await grantMutation.mutateAsync({ + prodEnvId, + scopeName, + consumerUsername, + grant, + }); + toast({ + title: `Scope ${scopeName} ${grant ? 'assigned' : 'removed'}`, + status: 'success', + }); + setBusy.off(); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + title: 'Scope update failed', + description: err?.message, + status: 'error', + }); + setBusy.off(); + } + }, + [client, queryKey, grantMutation, toast, busy] + ); + + if (isLoading || isFetching || busy) { + return ; + } + if (data == null) { + return Error; + } + if (data.consumerScopesAndRoles.id == '') { + return <>; + } + const clientScopes = credentialIssuer.availableScopes + ? JSON.parse(credentialIssuer.availableScopes) + : []; + + return ( + + + {clientScopes.map((scope) => ( + + + {scope} + + + ))} + + + ); +}; + +export default ScopesComponent; + +const mutation = gql` + mutation ToggleConsumerScopes( + $prodEnvId: ID! + $consumerUsername: String! + $scopeName: String! + $grant: Boolean! + ) { + updateConsumerScopeAssignment( + prodEnvId: $prodEnvId + consumerUsername: $consumerUsername + scopeName: $scopeName + grant: $grant + ) + } +`; + +const query = gql` + query GetConsumerScopesAndRoles($prodEnvId: ID!, $consumerUsername: ID!) { + consumerScopesAndRoles( + prodEnvId: $prodEnvId + consumerUsername: $consumerUsername + ) { + id + consumerType + defaultScopes + optionalScopes + clientRoles + } + } +`; diff --git a/src/nextapp/components/consumer-permissions/consumer-permissions.tsx b/src/nextapp/components/consumer-permissions/consumer-permissions.tsx deleted file mode 100644 index e6df577f4..000000000 --- a/src/nextapp/components/consumer-permissions/consumer-permissions.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react'; -import { Product, Environment } from '@/shared/types/query.types'; -import { - FormControl, - FormLabel, - Box, - Checkbox, - CheckboxGroup, - Divider, - Grid, - Switch, - Text, - Wrap, - WrapItem, - useToast, -} from '@chakra-ui/react'; -import { gql } from 'graphql-request'; -import { QueryKey, useQueryClient } from 'react-query'; -import { useApiMutation } from '@/shared/services/api'; -import RolesComponent from './roles'; - -interface ConsumerACLProps { - queryKey: any[]; - consumerId: string; - consumerUsername: string; - products: Product[]; -} - -const ConsumerPermissions: React.FC = ({ - queryKey, - consumerId, - consumerUsername, - products, -}) => { - return ( - - - Roles - Scopes - {products.map((product) => { - return product.environments - .filter((env) => env.credentialIssuer != null) - .map((env) => ( - <> - - {product.name}{' '} - - {env.name} - - - - - - )); - })} - - ); -}; - -export default ConsumerPermissions; diff --git a/src/nextapp/components/consumer-permissions/index.ts b/src/nextapp/components/consumer-permissions/index.ts deleted file mode 100644 index 493f53fc9..000000000 --- a/src/nextapp/components/consumer-permissions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './consumer-permissions'; diff --git a/src/nextapp/pages/devportal/requests/new/[id].tsx b/src/nextapp/pages/devportal/requests/new/[id].tsx index 9d3e59f18..948b76477 100644 --- a/src/nextapp/pages/devportal/requests/new/[id].tsx +++ b/src/nextapp/pages/devportal/requests/new/[id].tsx @@ -34,12 +34,10 @@ import { dehydrate } from 'react-query/hydration'; import { FieldsetBox, RadioGroup } from '@/components/forms'; import { FaBook } from 'react-icons/fa'; import { useRouter } from 'next/router'; -import isString from 'lodash/isString'; +import isNotBlank from '@/shared/isNotBlank'; const queryKey = 'newAccessRequest'; -const isNotBlank = (v: any) => isString(v) && v.length > 0; - export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; const queryClient = new QueryClient(); diff --git a/src/nextapp/pages/manager/consumers/[id].tsx b/src/nextapp/pages/manager/consumers/[id].tsx index bb46ef5d7..a5f501dce 100644 --- a/src/nextapp/pages/manager/consumers/[id].tsx +++ b/src/nextapp/pages/manager/consumers/[id].tsx @@ -21,8 +21,8 @@ import ControlsList from '@/components/controls-list'; import IpRestriction from '@/components/controls/ip-restriction'; import RateLimiting from '@/components/controls/rate-limiting'; import ModelIcon from '@/components/model-icon/model-icon'; +import ConsumerAuthz from '@/components/consumer-authz'; import ConsumerACL from '@/components/consumer-acl'; -import ConsumerPermissions from '@/components/consumer-permissions'; import breadcrumbs from '@/components/ns-breadcrumb'; @@ -78,15 +78,6 @@ const ConsumersPage: React.FC< ).length != 0 ).length != 0; - const hasEnvironmentWithClientCredFlow = (products: Product[]): boolean => - products - .filter((p) => p.environments.length != 0) - .filter( - (p) => - p.environments.filter((e) => e.flow == 'client-credentials').length != - 0 - ).length != 0; - return ( consumer && ( <> @@ -141,34 +132,19 @@ const ConsumersPage: React.FC< data={consumer.plugins.filter((p) => p.route || p.service)} /> - {hasEnvironmentWithAclBasedFlow(data.allProductsByNamespace) && ( - <> - - Authorization - ACL Groups - - - - - )} + - {hasEnvironmentWithClientCredFlow(data.allProductsByNamespace) && ( - <> - - Authorization - Scopes and Roles - - - - + {hasEnvironmentWithAclBasedFlow(data.allProductsByNamespace) && ( + )} @@ -205,6 +181,20 @@ const query = gql` createdAt } + allServiceAccesses(where: { consumer: { id: $id } }) { + name + consumerType + application { + appId + name + owner { + name + username + email + } + } + } + allProductsByNamespace { id name @@ -215,6 +205,7 @@ const query = gql` active flow credentialIssuer { + id availableScopes clientRoles } diff --git a/src/nextapp/pages/manager/requests/[id].tsx b/src/nextapp/pages/manager/requests/[id].tsx index f7df19c52..2d9aefc54 100644 --- a/src/nextapp/pages/manager/requests/[id].tsx +++ b/src/nextapp/pages/manager/requests/[id].tsx @@ -43,9 +43,7 @@ import RequestActions from '@/components/request-actions'; import BusinessProfile from '@/components/business-profile'; import ActivityList from '@/components/activity-list'; import breadcrumbs from '@/components/ns-breadcrumb'; -import isString from 'lodash/isString'; - -const isNotBlank = (v: any) => isString(v) && v.length > 0; +import isNotBlank from '@/shared/isNotBlank'; export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; diff --git a/src/nextapp/shared/types/query.types.ts b/src/nextapp/shared/types/query.types.ts index f8b292a2d..13b110251 100644 --- a/src/nextapp/shared/types/query.types.ts +++ b/src/nextapp/shared/types/query.types.ts @@ -7142,6 +7142,7 @@ export type Mutation = { updateConsumerGroupMembership?: Maybe; linkConsumerToNamespace?: Maybe; updateConsumerRoleAssignment?: Maybe; + updateConsumerScopeAssignment?: Maybe; createNamespace?: Maybe; deleteNamespace?: Maybe; createServiceAccount?: Maybe; @@ -7885,6 +7886,14 @@ export type MutationUpdateConsumerRoleAssignmentArgs = { }; +export type MutationUpdateConsumerScopeAssignmentArgs = { + prodEnvId: Scalars['ID']; + consumerUsername: Scalars['String']; + scopeName: Scalars['String']; + grant: Scalars['Boolean']; +}; + + export type MutationCreateNamespaceArgs = { namespace: Scalars['String']; }; diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index d09df7d70..8734e2a0c 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -183,7 +183,7 @@ export class KeycloakClientRegistrationService { public async applyChanges( clientId: string, - changes: string, + changes: string[][], optional: boolean ): Promise { const addFunction = optional @@ -200,6 +200,25 @@ export class KeycloakClientRegistrationService { } } + public async syncClientScopes( + subjectClientId: string, + clientUniqueId: string, + addScopes: string[], + delScopes: string[] + ) { + const lkup = await this.kcAdminClient.clients.find({ + clientId: subjectClientId, + }); + assert.strictEqual( + lkup.length, + 1, + 'Client ID not found ' + subjectClientId + ); + const clientPK = lkup[0].id; + + await this.applyChanges(clientPK, [addScopes, delScopes], false); + } + public async syncAndApply( clientId: string, desiredSetOfDefaultScopes: string[], diff --git a/src/services/keycloak/client-service.ts b/src/services/keycloak/client-service.ts index c21aac4fa..9e304c075 100644 --- a/src/services/keycloak/client-service.ts +++ b/src/services/keycloak/client-service.ts @@ -41,7 +41,7 @@ export class KeycloakClientService { public async lookupServiceAccountUserId(id: string) { const us = await this.kcAdminClient.clients.getServiceAccountUser({ id }); - logger.debug('[lookupServiceAccountUserId] (%d) RESULT %j', id, us); + logger.debug('[lookupServiceAccountUserId] (%s) RESULT %j', id, us); return us.id; } @@ -53,6 +53,15 @@ export class KeycloakClientService { return roles; } + public async listDefaultScopes(id: string) { + logger.debug('[listDefaultScopes] For %s', id); + const scopes = await this.kcAdminClient.clients.listDefaultClientScopes({ + id, + }); + logger.debug('[listDefaultScopes] RESULT %j', scopes); + return scopes; + } + public async login(clientId: string, clientSecret: string): Promise { await this.kcAdminClient .auth({ diff --git a/src/services/keystone/types.ts b/src/services/keystone/types.ts index f8b292a2d..13b110251 100644 --- a/src/services/keystone/types.ts +++ b/src/services/keystone/types.ts @@ -7142,6 +7142,7 @@ export type Mutation = { updateConsumerGroupMembership?: Maybe; linkConsumerToNamespace?: Maybe; updateConsumerRoleAssignment?: Maybe; + updateConsumerScopeAssignment?: Maybe; createNamespace?: Maybe; deleteNamespace?: Maybe; createServiceAccount?: Maybe; @@ -7885,6 +7886,14 @@ export type MutationUpdateConsumerRoleAssignmentArgs = { }; +export type MutationUpdateConsumerScopeAssignmentArgs = { + prodEnvId: Scalars['ID']; + consumerUsername: Scalars['String']; + scopeName: Scalars['String']; + grant: Scalars['Boolean']; +}; + + export type MutationCreateNamespaceArgs = { namespace: Scalars['String']; }; diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index ff4870d45..f0656bf24 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -51,7 +51,7 @@ export interface IssuerEnvironmentConfig { initialAccessToken?: string; } -export function getIssuerEnvironmentConfig( +export function checkIssuerEnvironmentConfig( issuer: CredentialIssuer, environment: string ) { @@ -59,10 +59,19 @@ export function getIssuerEnvironmentConfig( issuer.environmentDetails ); const env = details.filter((c) => c.environment === environment); + return env.length == 1 ? env[0] : null; +} + +export function getIssuerEnvironmentConfig( + issuer: CredentialIssuer, + environment: string +) { + const env = checkIssuerEnvironmentConfig(issuer, environment); + assert.strictEqual( - env.length, - 1, + env != null, + true, `EnvironmentMissing ${issuer.name} ${environment}` ); - return env[0]; + return env; } From 63ab83379fdd7b2f78848db47f16c809d2be35df Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 20:39:32 -0700 Subject: [PATCH 10/21] fix plugin maintenance --- .../controls-list/controls-list.tsx | 26 ++++++++++++------- .../components/controls/ip-restriction.tsx | 14 +++++++--- src/nextapp/components/controls/queries.ts | 18 ++++++++++++- .../components/controls/rate-limiting.tsx | 8 ++++-- src/services/kong/consumer-service.ts | 9 ++----- 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/src/nextapp/components/controls-list/controls-list.tsx b/src/nextapp/components/controls-list/controls-list.tsx index 7d37df2c6..c5f9d9ad0 100644 --- a/src/nextapp/components/controls-list/controls-list.tsx +++ b/src/nextapp/components/controls-list/controls-list.tsx @@ -88,20 +88,23 @@ const ControlsList: React.FC = ({ consumerId, data }) => { {d.name === 'ip-restriction' && ( )} {d.name === 'rate-limiting' && ( )} - + @@ -127,21 +130,24 @@ const ControlsList: React.FC = ({ consumerId, data }) => { {d.name === 'ip-restriction' && ( )} {d.name === 'rate-limiting' && ( )} - + diff --git a/src/nextapp/components/controls/ip-restriction.tsx b/src/nextapp/components/controls/ip-restriction.tsx index c0b2dd475..dc999db9a 100644 --- a/src/nextapp/components/controls/ip-restriction.tsx +++ b/src/nextapp/components/controls/ip-restriction.tsx @@ -12,7 +12,7 @@ import { useQueryClient, QueryKey } from 'react-query'; import ControlsDialog from './controls-dialog'; import ControlTypeSelect from './control-type-select'; -import { FULFILL_REQUEST } from './queries'; +import { CREATE_PLUGIN, UPDATE_PLUGIN } from './queries'; type ControlsPayload = { name: string; @@ -42,9 +42,11 @@ const IpRestriction: React.FC = ({ queryKey, }) => { const client = useQueryClient(); - const mutation = useApiMutation<{ id: string; controls: string }>( - FULFILL_REQUEST - ); + const mutation = useApiMutation<{ + id: string; + controls: string; + pluginExtForeignKey?: string; + }>(mode == 'edit' ? UPDATE_PLUGIN : CREATE_PLUGIN); const toast = useToast(); const config = data?.config ? JSON.parse(data.config) @@ -78,6 +80,10 @@ const IpRestriction: React.FC = ({ id, controls: JSON.stringify(controls), }; + if (mode == 'edit') { + payload['pluginExtForeignKey'] = data.extForeignKey; + } + await mutation.mutateAsync(payload); client.invalidateQueries(queryKey); toast({ diff --git a/src/nextapp/components/controls/queries.ts b/src/nextapp/components/controls/queries.ts index cd88df37d..195811f09 100644 --- a/src/nextapp/components/controls/queries.ts +++ b/src/nextapp/components/controls/queries.ts @@ -1,9 +1,25 @@ import { gql } from 'graphql-request'; -export const FULFILL_REQUEST = gql` +export const CREATE_PLUGIN = gql` mutation createGatewayConsumerPlugin($id: ID!, $controls: String!) { createGatewayConsumerPlugin(id: $id, plugin: $controls) { id } } `; + +export const UPDATE_PLUGIN = gql` + mutation updateGatewayConsumerPlugin( + $id: ID! + $pluginExtForeignKey: String! + $controls: String! + ) { + updateGatewayConsumerPlugin( + id: $id + pluginExtForeignKey: $pluginExtForeignKey + plugin: $controls + ) { + id + } + } +`; diff --git a/src/nextapp/components/controls/rate-limiting.tsx b/src/nextapp/components/controls/rate-limiting.tsx index 1c6b0747d..79913bff8 100644 --- a/src/nextapp/components/controls/rate-limiting.tsx +++ b/src/nextapp/components/controls/rate-limiting.tsx @@ -13,7 +13,7 @@ import { useApiMutation } from '@/shared/services/api'; import ControlsDialog from './controls-dialog'; import ControlTypeSelect from './control-type-select'; -import { FULFILL_REQUEST } from './queries'; +import { CREATE_PLUGIN, UPDATE_PLUGIN } from './queries'; type ControlsPayload = { name: string; @@ -49,7 +49,7 @@ const RateLimiting: React.FC = ({ }) => { const client = useQueryClient(); const mutation = useApiMutation<{ id: string; controls: string }>( - FULFILL_REQUEST + mode == 'edit' ? UPDATE_PLUGIN : CREATE_PLUGIN ); const toast = useToast(); const config = data?.config @@ -96,6 +96,10 @@ const RateLimiting: React.FC = ({ id, controls: JSON.stringify(controls), }; + if (mode == 'edit') { + payload['pluginExtForeignKey'] = data.extForeignKey; + } + await mutation.mutateAsync(payload); client.invalidateQueries(queryKey); toast({ diff --git a/src/services/kong/consumer-service.ts b/src/services/kong/consumer-service.ts index e2dab8a91..559ebe0e2 100644 --- a/src/services/kong/consumer-service.ts +++ b/src/services/kong/consumer-service.ts @@ -169,18 +169,13 @@ export class KongConsumerService { pluginPK: string, plugin: KongPlugin ): Promise { - const { v4: uuidv4 } = require('uuid'); - - const body = { - key: uuidv4().replace(/-/g, ''), - }; - logger.debug('CALLING with ' + consumerPK + ' ' + pluginPK); + logger.debug('[updateConsumerPlugin] C=%s P=%s', consumerPK, pluginPK); const response = await fetch( `${this.kongUrl}/consumers/${consumerPK}/plugins/${pluginPK}`, { method: 'put', - body: JSON.stringify(body), + body: JSON.stringify(plugin), headers: { 'Content-Type': 'application/json', }, From 5ae95a969439411539e9fd245d0e74b7327d8bfb Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 20:43:10 -0700 Subject: [PATCH 11/21] add missing shared lib --- src/nextapp/shared/isNotBlank.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/nextapp/shared/isNotBlank.ts diff --git a/src/nextapp/shared/isNotBlank.ts b/src/nextapp/shared/isNotBlank.ts new file mode 100644 index 000000000..fe80a8dd4 --- /dev/null +++ b/src/nextapp/shared/isNotBlank.ts @@ -0,0 +1,5 @@ +import isString from 'lodash/isString'; + +const isNotBlank = (v: any) => isString(v) && v.length > 0; + +export default isNotBlank; From 46e5a7cf11a3954fef5cfe7d5f25da73958467d2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 20:57:56 -0700 Subject: [PATCH 12/21] upd plugin tweaks --- src/authz/matrix.csv | 1 + src/authz/whitelist.json | 10 ++ src/lists/GatewayConsumer.js | 245 +++++++++++++++++++---------------- 3 files changed, 146 insertions(+), 110 deletions(-) diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index 5afe27d86..da8342163 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -52,6 +52,7 @@ API Owner Role Rules,,,TemporaryIdentity,read,,,,,,,"api-owner,provider-user",al API Owner Role Rules,,,User,read,,,,,,,"api-owner,provider-user",allow, API Owner Role Rules,,getGatewayConsumerPlugins,,,,,,,api-owner,,,allow, API Owner Role Rules,,deleteGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateGatewayConsumerPlugin,,,,,,,api-owner,,,allow, API Owner Role Rules,,createGatewayConsumerPlugin,,,,,,,api-owner,,,allow, API Owner Role Rules,,getPermissionTicketsForResource,,,,,,,api-owner,,,allow, API Owner Role Rules,,allPermissionTickets,,,,,,,api-owner,,,allow, diff --git a/src/authz/whitelist.json b/src/authz/whitelist.json index dbc06ec2b..93a0fb5a5 100644 --- a/src/authz/whitelist.json +++ b/src/authz/whitelist.json @@ -776,5 +776,15 @@ "referer": "http://localhost:4180/manager/consumers/60ad8e1044cf9a36ef674085", "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n name\n username\n email\n }\n }\n }\n\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", "added": "2021-07-03T01:47:59.254Z" + }, + "b2e2c4b6ce7586668c34d3226ae6c5da": { + "referer": "http://localhost:4180/manager/consumers/60b7e9154b74934a7b6ad72a", + "query": "\n mutation updateGatewayConsumerPlugin($id: ID!, $controls: String!) {\n updateGatewayConsumerPlugin(id: $id, plugin: $controls) {\n id\n }\n }\n", + "added": "2021-07-03T03:10:14.327Z" + }, + "7073ccad4a36f9d6b1fae48ad9e00f72": { + "referer": "http://localhost:4180/manager/consumers/60b7e9154b74934a7b6ad72a", + "query": "\n mutation updateGatewayConsumerPlugin(\n $id: ID!\n $pluginExtForeignKey: String!\n $controls: String!\n ) {\n updateGatewayConsumerPlugin(\n id: $id\n pluginExtForeignKey: $pluginExtForeignKey\n plugin: $controls\n ) {\n id\n }\n }\n", + "added": "2021-07-03T03:23:01.128Z" } } \ No newline at end of file diff --git a/src/lists/GatewayConsumer.js b/src/lists/GatewayConsumer.js index 6fd3e0c9b..47d73a0d5 100644 --- a/src/lists/GatewayConsumer.js +++ b/src/lists/GatewayConsumer.js @@ -1,33 +1,36 @@ -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 { externallySourced } = require('../components/ExternalSource') +const { externallySourced } = require('../components/ExternalSource'); -const { byTracking, atTracking } = require('@keystonejs/list-plugins') +const { byTracking, atTracking } = require('@keystonejs/list-plugins'); -const { EnforcementPoint } = require('../authz/enforcement') +const { EnforcementPoint } = require('../authz/enforcement'); -const { lookupConsumerPlugins, lookupKongConsumerId } = require('../services/keystone') +const { + lookupConsumerPlugins, + lookupKongConsumerId, +} = require('../services/keystone'); -const { KongConsumerService } = require('../services/kong') -const { FeederService } = require('../services/feeder') +const { KongConsumerService } = require('../services/kong'); +const { FeederService } = require('../services/feeder'); module.exports = { fields: { username: { - type: Text, - isRequired: true, - isUnique: true, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: true, + isUnique: true, + adminConfig: { + isReadOnly: true, + }, }, customId: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, // kongConsumerId: { // type: Text, @@ -37,103 +40,125 @@ module.exports = { // } // }, aclGroups: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, namespace: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, tags: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, - plugins: { type: Relationship, ref: 'GatewayPlugin', many: true } + plugins: { type: Relationship, ref: 'GatewayPlugin', many: true }, }, access: EnforcementPoint, - plugins: [ - externallySourced(), - atTracking() - ], + plugins: [externallySourced(), atTracking()], extensions: [ - (keystone) => { - keystone.extendGraphQLSchema({ - queries: [ - { - schema: 'getGatewayConsumerPlugins(id: ID!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - return await lookupConsumerPlugins (noauthContext, args.id ) - }, - access: EnforcementPoint, - }, - ], - mutations: [ - { - schema: 'createGatewayConsumerPlugin(id: ID!, plugin: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.addPluginToConsumer (kongConsumerPK, JSON.parse(args.plugin) ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - }, - { - schema: 'updateGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!, plugin: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.updateConsumerPlugin (kongConsumerPK, args.pluginExtForeignKey, JSON.parse(args.plugin) ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - }, - { - schema: 'deleteGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.deleteConsumerPlugin (kongConsumerPK, args.pluginExtForeignKey ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - } - ] - }); - } - ] - -} + (keystone) => { + keystone.extendGraphQLSchema({ + queries: [ + { + schema: 'getGatewayConsumerPlugins(id: ID!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + return await lookupConsumerPlugins(noauthContext, args.id); + }, + access: EnforcementPoint, + }, + ], + mutations: [ + { + schema: + 'createGatewayConsumerPlugin(id: ID!, plugin: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.addPluginToConsumer( + kongConsumerPK, + JSON.parse(args.plugin) + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + { + schema: + 'updateGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!, plugin: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.updateConsumerPlugin( + kongConsumerPK, + args.pluginExtForeignKey, + JSON.parse(args.plugin) + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + { + schema: + 'deleteGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.deleteConsumerPlugin( + kongConsumerPK, + args.pluginExtForeignKey + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + ], + }); + }, + ], +}; From 3e7f7a9c38fea64588150907248f595f1a066d56 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 21:24:43 -0700 Subject: [PATCH 13/21] upd proxy to skip feed traffic --- .github/workflows/ci-build-deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index 38157814a..4f9ff74a1 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -177,7 +177,7 @@ jobs: client-secret: ${{ secrets.OIDC_CLIENT_SECRET }} oidc-issuer-url: ${{ secrets.OIDC_ISSUER }} redirect-url: https://api-services-portal-${{ steps.set-deploy-id.outputs.DEPLOY_ID }}.apps.silver.devops.gov.bc.ca/oauth2/callback - skip-auth-regex: '/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/ds/api|/signout|^[/]$' + skip-auth-regex: '/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/feed/|/ds/api|/signout|^[/]$' whitelist-domain: authz-apps-gov-bc-ca.dev.api.gov.bc.ca skip-provider-button: 'true' profile-url: ${{ secrets.OIDC_ISSUER }}/protocol/openid-connect/userinfo From ac70ad20fec168093616052c3591d3fe41f1ebe4 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 21:44:15 -0700 Subject: [PATCH 14/21] add TZ to feeder --- .github/workflows/ci-build-feeders.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-build-feeders.yaml b/.github/workflows/ci-build-feeders.yaml index 080cb1d6f..e16bc7f66 100644 --- a/.github/workflows/ci-build-feeders.yaml +++ b/.github/workflows/ci-build-feeders.yaml @@ -125,6 +125,8 @@ jobs: name: proto-asp-${{ steps.set-deploy-id.outputs.DEPLOY_ID }} env: + TZ: + value: 'America/Los_Angeles' LOG_FEEDS: value: 'false' WORKING_PATH: From c1a82f17e179956064990ee078377956d784eb69 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 22:31:09 -0700 Subject: [PATCH 15/21] upd directory title and markdown --- .../discovery-list/discovery-list-item.tsx | 14 ++-- src/nextapp/pages/devportal/access/index.tsx | 14 ++-- .../{api-discovery => api-directory}/[id].tsx | 64 +++++++++++++------ .../index.tsx | 4 +- src/nextapp/pages/index.tsx | 10 +-- src/nextapp/shared/data/links.ts | 8 +-- 6 files changed, 70 insertions(+), 44 deletions(-) rename src/nextapp/pages/devportal/{api-discovery => api-directory}/[id].tsx (72%) rename src/nextapp/pages/devportal/{api-discovery => api-directory}/index.tsx (95%) diff --git a/src/nextapp/components/discovery-list/discovery-list-item.tsx b/src/nextapp/components/discovery-list/discovery-list-item.tsx index c2bf1b0c8..f135af784 100644 --- a/src/nextapp/components/discovery-list/discovery-list-item.tsx +++ b/src/nextapp/components/discovery-list/discovery-list-item.tsx @@ -45,12 +45,12 @@ const DiscoveryListItem: React.FC = ({ data }) => { {data.dataset ? ( <> - + {data.dataset.title} ) : ( - + {data.name} )} @@ -60,10 +60,10 @@ const DiscoveryListItem: React.FC = ({ data }) => { - {data.organization && ( + {data.dataset?.organization && ( <> - {data.organization.title} - {data.organizationUnit && ( + {data.dataset.organization.title} + {data.dataset.organizationUnit && ( <> = ({ data }) => { mt={1} fontSize="xs" > - {data.organizationUnit.title} + {data.dataset.organizationUnit.title} )} )} - {!data.organization && 'Open Dataset'} + {!data.dataset?.organization && 'Open Dataset'} {data.dataset && ( diff --git a/src/nextapp/pages/devportal/access/index.tsx b/src/nextapp/pages/devportal/access/index.tsx index f7896af4d..b80960ff9 100644 --- a/src/nextapp/pages/devportal/access/index.tsx +++ b/src/nextapp/pages/devportal/access/index.tsx @@ -43,23 +43,21 @@ const ApiAccessPage: React.FC< API Program Services | API Access - - + - + List of the BC Government Service APIs that you have access to. - + - {data.myServiceAccesses.length == 0 && ( + message="Go to the Directory to find one today!" + title="Not using any APIs yet?" + /> )} diff --git a/src/nextapp/pages/devportal/api-discovery/[id].tsx b/src/nextapp/pages/devportal/api-directory/[id].tsx similarity index 72% rename from src/nextapp/pages/devportal/api-discovery/[id].tsx rename to src/nextapp/pages/devportal/api-directory/[id].tsx index 33c368067..604758743 100644 --- a/src/nextapp/pages/devportal/api-discovery/[id].tsx +++ b/src/nextapp/pages/devportal/api-directory/[id].tsx @@ -28,6 +28,14 @@ import { FaTimesCircle, } from 'react-icons/fa'; import TagsList from '@/components/tags-list'; +import ReactMarkdownWithHtml from 'react-markdown/with-html'; +import gfm from 'remark-gfm'; +import { DocHeader, InternalLink } from '@/components/docs'; + +const renderers = { + link: InternalLink, + heading: DocHeader, +}; type DetailItem = { title: string; @@ -89,7 +97,7 @@ const ApiPage: React.FC< return ( <> - API Program Services | API Discovery + API Services Portal | API Directory } title={ - - {data?.name} - - + data?.dataset?.isInCatalog ? ( + + {data?.name} + + + ) : ( + {data?.name} + ) } > + {data.dataset?.organization && ( + + + Published by the{' '} + + {data.dataset.organization.title} + {' - '} + + {data.dataset.organizationUnit && ( + {data.dataset.organizationUnit.title} + )} + + + )} Licensed under{' '} @@ -132,7 +158,9 @@ const ApiPage: React.FC< - {data.dataset?.notes} + + {data.dataset?.notes} + diff --git a/src/nextapp/pages/devportal/api-discovery/index.tsx b/src/nextapp/pages/devportal/api-directory/index.tsx similarity index 95% rename from src/nextapp/pages/devportal/api-discovery/index.tsx rename to src/nextapp/pages/devportal/api-directory/index.tsx index 2b960f735..104d93066 100644 --- a/src/nextapp/pages/devportal/api-discovery/index.tsx +++ b/src/nextapp/pages/devportal/api-directory/index.tsx @@ -38,10 +38,10 @@ const ApiDiscoveryPage: React.FC< return ( <> - API Program Services | API Discovery + API Services Portal | API Directory - + Find an API and request an API key to get started diff --git a/src/nextapp/pages/index.tsx b/src/nextapp/pages/index.tsx index 25a3de035..e286cdea1 100644 --- a/src/nextapp/pages/index.tsx +++ b/src/nextapp/pages/index.tsx @@ -22,19 +22,19 @@ type HomeActions = { }; const actions: HomeActions[] = [ { - title: 'Are you a Developer?', - url: '/devportal/api-discovery', + title: 'For Developers', + url: '/devportal/api-directory', icon: FaBook, roles: [], - description: `Looking for BC Government APIs to integrate with? Go to the Directory to see what is available and to request access!`, + description: `Visit the Directory to see what APIs are available for integration.`, }, { - title: 'Are you an API Provider?', + title: 'For API Providers', url: '/manager/namespaces', icon: FaToolbox, roles: [], description: - 'Is your Ministry looking to build and share APIs? Go to Namespaces to get started!', + 'Login with BC Government credentials to start building and sharing APIs from your Ministry', }, ]; diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index 8fb5d7145..7d3664ee5 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -14,14 +14,14 @@ const links: NavLink[] = [ // { name: 'Home', url: '/manager', access: [], sites: ['manager'] }, // { name: 'Home', url: '/devportal', access: [], sites: ['devportal'] }, { - name: 'Directory', - url: '/devportal/api-discovery', + name: 'API Directory', + url: '/devportal/api-directory', access: [], - altUrls: ['/devportal/api-discovery/[id]'], + altUrls: ['/devportal/api-directory/[id]'], sites: ['devportal'], }, { - name: 'API Access', + name: 'My Access', url: '/devportal/access', access: ['portal-user'], altUrls: [ From 0b99389fb34706083143d2d816c22fd9974b1d17 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Fri, 2 Jul 2021 22:47:11 -0700 Subject: [PATCH 16/21] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ab4783902..167e3afb7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # API Services Portal +[![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + ## Introduction The `API Services Portal` is a frontend for API Providers to manage the lifecycle of their APIs and for Developers to discover and access these APIs. It works in combination with the Kong Community Edition Gateway and Keycloak IAM solution. From 4fada9a4b7e9f2f86da7d1da1835713f0cce4fe2 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 5 Jul 2021 10:08:47 -0700 Subject: [PATCH 17/21] fix for not backfilling --- src/nextapp/pages/manager/namespace-access/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index 3c4631f09..ba829b66e 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -68,9 +68,10 @@ const AccessRedirectPage: React.FC< }, }, { - enabled: Boolean(data) && isSuccess, + enabled: Boolean(resourceId), } ); + const requests = permissions.data?.getPermissionTicketsForResource.filter( (p) => !p.granted ); From 28d19b8a7f34101ee8bf1d73887e0320aa94dd0b Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 5 Jul 2021 11:04:51 -0700 Subject: [PATCH 18/21] upd queryKey in namespace access --- .../pages/manager/namespace-access/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index ba829b66e..f1a2abada 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -17,7 +17,8 @@ import UsersAccessList from '@/components/users-access-list'; import ServiceAccountsList from '@/components/service-accounts-list'; export const getServerSideProps: GetServerSideProps = async (context) => { - const queryKey = 'namespaceAccess'; + const keystoneReq = context.req as any; + const queryKey = ['namespaceAccess', keystoneReq.user.namespace]; const queryClient = new QueryClient(); await queryClient.prefetchQuery( @@ -59,7 +60,7 @@ const AccessRedirectPage: React.FC< const prodEnvId = data?.currentNamespace?.prodEnvId; const permissions = useApi( - 'namespacePermissions', + ['namespacePermissions', resourceId], { query: permissionsQuery, variables: { @@ -68,7 +69,7 @@ const AccessRedirectPage: React.FC< }, }, { - enabled: Boolean(resourceId), + enabled: isSuccess && Boolean(resourceId), } ); @@ -113,12 +114,7 @@ const AccessRedirectPage: React.FC< /> - {!resourceId && ( - - )} + Date: Mon, 5 Jul 2021 11:31:48 -0700 Subject: [PATCH 19/21] return null when no current namespace --- src/lists/extensions/Namespace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index 79b15e12d..3c024e3fc 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -90,7 +90,7 @@ module.exports = { '[currentNamespace] NOT FOUND! %j', context.req.user ); - return {}; + return null; } else { return matched[0]; } From 078c973f7faf85cd4866737958a495477435d189 Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 5 Jul 2021 16:33:28 -0700 Subject: [PATCH 20/21] remove ns access prefetch --- src/authz/whitelist.json | 5 + .../pages/manager/namespace-access/index.tsx | 154 ++++++++++-------- 2 files changed, 87 insertions(+), 72 deletions(-) diff --git a/src/authz/whitelist.json b/src/authz/whitelist.json index 93a0fb5a5..f6660fa53 100644 --- a/src/authz/whitelist.json +++ b/src/authz/whitelist.json @@ -786,5 +786,10 @@ "referer": "http://localhost:4180/manager/consumers/60b7e9154b74934a7b6ad72a", "query": "\n mutation updateGatewayConsumerPlugin(\n $id: ID!\n $pluginExtForeignKey: String!\n $controls: String!\n ) {\n updateGatewayConsumerPlugin(\n id: $id\n pluginExtForeignKey: $pluginExtForeignKey\n plugin: $controls\n ) {\n id\n }\n }\n", "added": "2021-07-03T03:23:01.128Z" + }, + "a2750969742e7ffa0880f452f82ee58b": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) {\n grantPermissions(prodEnvId: $prodEnvId, data: $data) {\n id\n }\n }\n", + "added": "2021-07-05T23:29:33.635Z" } } \ No newline at end of file diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index f1a2abada..8321f01a0 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -5,7 +5,7 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { QueryClient } from 'react-query'; import { Query } from '@/shared/types/query.types'; import { dehydrate } from 'react-query/hydration'; -import { Box, Container, Divider, Heading } from '@chakra-ui/react'; +import { Box, Container, Divider, Heading, Skeleton } from '@chakra-ui/react'; import GrantAccessDialog from '@/components/grant-access-dialog'; import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; import PageHeader from '@/components/page-header'; @@ -16,30 +16,27 @@ import EmptyPane from '@/components/empty-pane'; import UsersAccessList from '@/components/users-access-list'; import ServiceAccountsList from '@/components/service-accounts-list'; +const Loading = ( + + + + +); + export const getServerSideProps: GetServerSideProps = async (context) => { const keystoneReq = context.req as any; - const queryKey = ['namespaceAccess', keystoneReq.user.namespace]; - const queryClient = new QueryClient(); - - await queryClient.prefetchQuery( - queryKey, - async () => - await api(query, { - headers: context.req.headers as HeadersInit, - }) - ); + const nsQueryKey = ['namespaceAccess', keystoneReq.user.namespace]; return { props: { - dehydratedState: dehydrate(queryClient), - queryKey, + nsQueryKey, }, }; }; const AccessRedirectPage: React.FC< InferGetServerSidePropsType -> = ({ queryKey }) => { +> = ({ nsQueryKey, headers }) => { const { user } = useAuth(); const breadcrumbs = user ? [ @@ -48,8 +45,8 @@ const AccessRedirectPage: React.FC< ] : []; - const { data, isSuccess } = useApi( - queryKey, + const { data, isSuccess, isLoading } = useApi( + nsQueryKey, { query }, { suspense: false, @@ -59,8 +56,14 @@ const AccessRedirectPage: React.FC< const resourceId = data?.currentNamespace?.id; const prodEnvId = data?.currentNamespace?.prodEnvId; - const permissions = useApi( - ['namespacePermissions', resourceId], + const queryKey: any = ['namespacePermissions', resourceId]; + + const { + data: permissions, + isSuccess: isPermissionsSuccess, + isLoading: isPermissionsLoading, + } = useApi( + queryKey, { query: permissionsQuery, variables: { @@ -73,19 +76,19 @@ const AccessRedirectPage: React.FC< } ); - const requests = permissions.data?.getPermissionTicketsForResource.filter( + const requests = permissions?.getPermissionTicketsForResource.filter( (p) => !p.granted ); return ( <> - {`API Program Services | Resources | ${permissions.data?.getResourceSet.type} ${permissions.data?.getResourceSet.name}`} + {`API Program Services | Resources | ${permissions?.getResourceSet.type} ${permissions?.getResourceSet.name}`} 0 && ( - - - Users with Access - - - - - p.granted - )} - resourceId={resourceId} - prodEnvId={prodEnvId} - queryKey={queryKey} - /> - - - - - Service Accounts with Access - - - - - + + {isLoading || isPermissionsLoading ? ( + Loading + ) : ( + <> + + + Users with Access + + + + + p.granted + )} + resourceId={resourceId} + prodEnvId={prodEnvId} + queryKey={queryKey} + /> + + + + + Service Accounts with Access + + + + + + + )} ); From d1744997d9d7231bc8bf09515f9510dd30e324bc Mon Sep 17 00:00:00 2001 From: ikethecoder Date: Mon, 5 Jul 2021 20:00:00 -0700 Subject: [PATCH 21/21] ninclude missing qmutations in whitelist --- src/authz/whitelist.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/authz/whitelist.json b/src/authz/whitelist.json index f6660fa53..501d8c79a 100644 --- a/src/authz/whitelist.json +++ b/src/authz/whitelist.json @@ -791,5 +791,15 @@ "referer": "http://localhost:4180/manager/namespace-access", "query": "\n mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) {\n grantPermissions(prodEnvId: $prodEnvId, data: $data) {\n id\n }\n }\n", "added": "2021-07-05T23:29:33.635Z" + }, + "d78565ea5ddbf2b4c6f47b7654cb52c4": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation GrantSAAccess(\n $prodEnvId: ID!\n $resourceId: String!\n $data: UMAPolicyInput!\n ) {\n createUmaPolicy(\n prodEnvId: $prodEnvId\n resourceId: $resourceId\n data: $data\n ) {\n id\n }\n }\n", + "added": "2021-07-06T02:58:10.429Z" + }, + "990f9d0261b747a2d571b0898a9680a8": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation RevokeSAAccess(\n $prodEnvId: ID!\n $resourceId: String!\n $policyId: String!\n ) {\n deleteUmaPolicy(\n prodEnvId: $prodEnvId\n resourceId: $resourceId\n policyId: $policyId\n )\n }\n", + "added": "2021-07-06T02:58:35.159Z" } } \ No newline at end of file