From 23a81783bf9f8945304f77136113402a6647daa1 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Wed, 18 Jan 2023 15:15:53 -0800 Subject: [PATCH 1/5] Add new JSM links --- .../new-organization-form.tsx | 8 ++- .../pages/manager/namespaces/index.tsx | 64 ++++++++++--------- src/server.ts | 2 + 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/nextapp/components/new-organization-form/new-organization-form.tsx b/src/nextapp/components/new-organization-form/new-organization-form.tsx index 2faa9e216..7b04d9990 100644 --- a/src/nextapp/components/new-organization-form/new-organization-form.tsx +++ b/src/nextapp/components/new-organization-form/new-organization-form.tsx @@ -21,10 +21,12 @@ import { useApi, useApiMutation } from '@/shared/services/api'; import { useQueryClient } from 'react-query'; import { queryKey } from '@/shared/hooks/use-current-namespace'; import { gql } from 'graphql-request'; +import { useGlobal } from '@/shared/services/global'; const NewOrganizationForm: React.FC = () => { const ref = React.useRef(null); const { user } = useAuth(); + const global = useGlobal(); const { isOpen, onClose, onOpen } = useDisclosure(); const mutate = useApiMutation(mutation); const client = useQueryClient(); @@ -130,7 +132,11 @@ const NewOrganizationForm: React.FC = () => { If you don’t know your Organization or Business Unit or it is not listed, please submit a request through the{' '} - + Data Systems and Services request system diff --git a/src/nextapp/pages/manager/namespaces/index.tsx b/src/nextapp/pages/manager/namespaces/index.tsx index 1108b87c4..a651a4796 100644 --- a/src/nextapp/pages/manager/namespaces/index.tsx +++ b/src/nextapp/pages/manager/namespaces/index.tsx @@ -52,6 +52,7 @@ import EmptyPane from '@/components/empty-pane'; import NamespaceMenu from '@/components/namespace-menu/namespace-menu'; import NewNamespace from '@/components/new-namespace'; import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; +import { useGlobal } from '@/shared/services/global'; const actions = [ { @@ -119,6 +120,7 @@ const NamespacesPage: React.FC = () => { const client = useQueryClient(); const namespace = useCurrentNamespace(); const { isOpen, onClose, onOpen } = useDisclosure(); + const global = useGlobal(); const currentOrg = React.useMemo(() => { if (namespace.isSuccess && namespace.data.currentNamespace?.org) { return { @@ -193,36 +195,38 @@ const NamespacesPage: React.FC = () => { {currentOrg.text} - - - - - - - - - - If you need to change the Organization or Business Unit for your - Namespace, submit a request through the{' '} - - Data Systems and Services request system - - - - - + {user?.roles.includes('api-owner') && ( + + + + + + + + + + If you need to change the Organization or Business Unit for + your Namespace, submit a request through the{' '} + + Data Systems and Services request system + + + + + + )} )} diff --git a/src/server.ts b/src/server.ts index 32ff0e9b5..73a4e98dd 100644 --- a/src/server.ts +++ b/src/server.ts @@ -376,6 +376,8 @@ const configureExpress = (app: any) => { helpSupportUrl: process.env.NEXT_PUBLIC_HELP_SUPPORT_URL, helpReleaseUrl: process.env.NEXT_PUBLIC_HELP_RELEASE_URL, helpStatusUrl: process.env.NEXT_PUBLIC_HELP_STATUS_URL, + helpAddOrgUrl: process.env.NEXT_PUBLIC_HELP_ADD_ORG_URL, + helpChangeOrgUrl: process.env.NEXT_PUBLIC_CHANGE_ORG_URL, }, }); }); From c1d2c9bbc2786a05fc331d8b1a728dd908bbc411 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Wed, 18 Jan 2023 17:21:04 -0800 Subject: [PATCH 2/5] Open add org in new tab --- .../components/new-organization-form/new-organization-form.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nextapp/components/new-organization-form/new-organization-form.tsx b/src/nextapp/components/new-organization-form/new-organization-form.tsx index 7b04d9990..ef562de1f 100644 --- a/src/nextapp/components/new-organization-form/new-organization-form.tsx +++ b/src/nextapp/components/new-organization-form/new-organization-form.tsx @@ -134,6 +134,8 @@ const NewOrganizationForm: React.FC = () => { listed, please submit a request through the{' '} From 64dcb268dc47134fec7362f8045bb7ed30a0bce3 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Thu, 19 Jan 2023 12:27:11 -0800 Subject: [PATCH 3/5] Sprint 61 UI Fixes (#683) * Fix login redirect * Update login dialog layout * Add empty message for service accounts * Fix up the results of a dataset query * Update copy in dataset --- src/mocks/handlers.js | 4 ++-- .../login-buttons/login-buttons.tsx | 1 + .../components/login-dialog/login-dialog.tsx | 23 +++++-------------- .../products-list/dataset-input.tsx | 22 ++++++++++-------- src/nextapp/pages/index.tsx | 20 +++++++++------- .../pages/manager/service-accounts/index.tsx | 10 ++++++-- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index ec38d7cca..68ba8c4b3 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -101,7 +101,7 @@ const allNamespaces = [ }, ]; let namespace = personas.mark.namespace; -let user = { ...personas.mark, namespace }; +let user = { ...personas.harley, namespace }; export function resetAll() { consumersStore.reset(); @@ -170,7 +170,7 @@ export const handlers = [ return res( ctx.status(200), ctx.json({ - user: { ...personas.mark, namespace }, + user: { ...user, namespace }, }) ); }), diff --git a/src/nextapp/components/login-buttons/login-buttons.tsx b/src/nextapp/components/login-buttons/login-buttons.tsx index 5162f00ee..a2309e04e 100644 --- a/src/nextapp/components/login-buttons/login-buttons.tsx +++ b/src/nextapp/components/login-buttons/login-buttons.tsx @@ -83,6 +83,7 @@ const LoginButtons: React.FC = ({ href={href} leftIcon={icon} bgColor={bgColor} + w="100%" data-testid={`login-with-${kebabCase(button.text)}`} > Login with {button.text} diff --git a/src/nextapp/components/login-dialog/login-dialog.tsx b/src/nextapp/components/login-dialog/login-dialog.tsx index 5c3e0472b..5e6393073 100644 --- a/src/nextapp/components/login-dialog/login-dialog.tsx +++ b/src/nextapp/components/login-dialog/login-dialog.tsx @@ -11,6 +11,7 @@ import { useDisclosure, ButtonProps, Flex, + VStack, } from '@chakra-ui/react'; import LoginButtons from '../login-buttons'; @@ -27,7 +28,6 @@ const LoginDialog: React.FC = ({ }) => { const { isOpen, onOpen, onClose } = useDisclosure(); const { identities, identityContent } = useGlobal(); - const size = identities.developer.length > 2 ? '2xl' : 'lg'; const isInline = buttonVariant === 'link'; const buttonProps = !isInline ? {} @@ -43,36 +43,25 @@ const LoginDialog: React.FC = ({ - + Login to request access - - Choose which of the options you want to authenticate with. You - will go to a secure website to log in and automatically return.{' '} - - Access to certain features of the APS portal will be based on the - account type that you choose. + You will go to a secure website to log in and automatically + return. - + - + diff --git a/src/nextapp/components/products-list/dataset-input.tsx b/src/nextapp/components/products-list/dataset-input.tsx index 04337101e..aabfbce21 100644 --- a/src/nextapp/components/products-list/dataset-input.tsx +++ b/src/nextapp/components/products-list/dataset-input.tsx @@ -91,7 +91,6 @@ const DatasetInput: React.FC = ({ dataset }) => { getLabelProps, getMenuProps, isOpen, - inputValue, highlightedIndex, selectedItem, getRootProps, @@ -101,8 +100,9 @@ const DatasetInput: React.FC = ({ dataset }) => { Link to BC Data Catalogue - This value is the slug value of a corresponding BC Data - Catalogue entry: https://catalogue.data.gov.bc.ca/dataset/ + Enter an existing dataset. This value is the slug value of a + corresponding BCDC entry: + https://catalogue.data.gov.bc.ca/dataset/ {selected ? selected.name : ''} = ({ dataset }) => { /> {isOpen && @@ -164,6 +161,13 @@ const DatasetInput: React.FC = ({ dataset }) => { {d.title} ))} + {isOpen && isSuccess && !results.length && ( + + + No results found + + + )} )} diff --git a/src/nextapp/pages/index.tsx b/src/nextapp/pages/index.tsx index e286cdea1..410dfaa35 100644 --- a/src/nextapp/pages/index.tsx +++ b/src/nextapp/pages/index.tsx @@ -4,18 +4,13 @@ import Head from 'next/head'; import NextLink from 'next/link'; import Card from '@/components/card'; import GridLayout from '@/layouts/grid'; -import { - FaBook, - FaDatabase, - FaServer, - FaShieldAlt, - FaToolbox, -} from 'react-icons/fa'; +import { FaBook, FaToolbox } from 'react-icons/fa'; import { useAuth } from '@/shared/services/auth'; type HomeActions = { title: string; url: string; + fallbackUrl?: string; icon: React.ComponentType; roles: string[]; description: string; @@ -31,6 +26,7 @@ const actions: HomeActions[] = [ { title: 'For API Providers', url: '/manager/namespaces', + fallbackUrl: '/login?identity=provider&f=%2F', icon: FaToolbox, roles: [], description: @@ -40,6 +36,7 @@ const actions: HomeActions[] = [ const HomePage: React.FC = () => { const { user } = useAuth(); + const isLoggedOut = !user; return ( <> @@ -78,7 +75,14 @@ const HomePage: React.FC = () => { - + {action.title} diff --git a/src/nextapp/pages/manager/service-accounts/index.tsx b/src/nextapp/pages/manager/service-accounts/index.tsx index ed6801214..5e8fb4951 100644 --- a/src/nextapp/pages/manager/service-accounts/index.tsx +++ b/src/nextapp/pages/manager/service-accounts/index.tsx @@ -5,8 +5,6 @@ import { Button, Container, Text, - Divider, - Heading, Modal, ModalOverlay, ModalContent, @@ -36,6 +34,7 @@ import { format } from 'date-fns'; import { FaCheckCircle } from 'react-icons/fa'; import ServiceAccountCreate from '@/components/service-account-create'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; +import EmptyPane from '@/components/empty-pane'; export const getServerSideProps: GetServerSideProps = async (context) => { const queryKey = 'getServiceAccounts'; @@ -146,6 +145,13 @@ const ServiceAccountsPage: React.FC< } + /> + } columns={[ { name: 'ID', key: 'active', sortable: true }, { name: 'Created On', key: 'name', sortable: true }, From 84ed654cc46e9dd93cccc7d40edb8db9436da82e Mon Sep 17 00:00:00 2001 From: ike thecoder Date: Fri, 20 Jan 2023 13:25:59 -0800 Subject: [PATCH 4/5] owner email fix inherit from name fix plugin template fix (#716) * owner email fix inherit from name fix plugin template fix * workaround to address build error * match namespace and namespacedata for id being optional * create 400 for any error in batch worker * upd example for issuer api * Remove old NamespaceData types * Remove any casting now that namespace type is correct Co-authored-by: Joshua Jones --- .../actions/filterByUserNSorSharedTrue.js | 7 + src/authz/enforcement.ts | 1 + src/authz/matrix.csv | 4 +- src/batch/data-rules.js | 6 +- src/batch/feed-worker.ts | 306 +++++++++--------- src/lists/extensions/CredentialIssuerExt.ts | 34 +- src/lists/extensions/UserExt.ts | 42 +++ .../templates/jwt-keycloak.ts | 19 +- .../templates/kong-acl-only.ts | 2 +- .../templates/kong-api-key-acl.ts | 2 +- .../namespace-manager/namespace-manager.tsx | 4 +- .../namespace-menu/namespace-menu.tsx | 16 +- src/nextapp/shared/types/app.types.ts | 2 +- src/nextapp/shared/types/query.types.ts | 17 +- src/server.ts | 1 + .../keycloak/client-registration-service.ts | 6 +- .../templates/protocol-mappers/audience.ts | 4 +- src/services/keystone/types.ts | 17 +- src/services/keystone/user.ts | 25 ++ src/test/integrated/keystonejs/user.ts | 7 +- 20 files changed, 338 insertions(+), 184 deletions(-) create mode 100644 src/authz/actions/filterByUserNSorSharedTrue.js create mode 100644 src/lists/extensions/UserExt.ts diff --git a/src/authz/actions/filterByUserNSorSharedTrue.js b/src/authz/actions/filterByUserNSorSharedTrue.js new file mode 100644 index 000000000..d19c8738a --- /dev/null +++ b/src/authz/actions/filterByUserNSorSharedTrue.js @@ -0,0 +1,7 @@ +const actionFilterNSorSharedTrue = (context, value) => { + const namespace = context['user']['namespace']; + + return { OR: [{ namespace: namespace }, { isShared: true }] }; +}; + +module.exports = actionFilterNSorSharedTrue; diff --git a/src/authz/enforcement.ts b/src/authz/enforcement.ts index c1863aea6..53f5d72c1 100644 --- a/src/authz/enforcement.ts +++ b/src/authz/enforcement.ts @@ -63,6 +63,7 @@ const actions: any = { filterByProductNSOrActiveEnvironment: require('./actions/filterByProductNSOrActiveEnvironment'), filterByTemporaryIdentity: require('./actions/filterByTemporaryIdentity'), filterByUserNS: require('./actions/filterByUserNS'), + filterByUserNSorSharedTrue: require('./actions/filterByUserNSorSharedTrue'), filterByUserNSOrNull: require('./actions/filterByUserNSOrNull'), filterByActive: require('./actions/filterByActive'), filterByActiveEnvironment: require('./actions/filterByActiveEnvironment'), diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index 5d5d6ea7d..d370c7956 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -47,10 +47,12 @@ ACCESS MANAGER,,,GatewayConsumer,read,,,,,access-manager,,,allow, ACCESS MANAGER,,BusinessProfile,,,,,,,access-manager,,,allow, CREDENTIAL ADMIN,,,CredentialIssuer,,"update,delete",,,,credential-admin,,,allow,filterByUserNS CREDENTIAL ADMIN,,,CredentialIssuer,create,,,,,credential-admin,,,allow, -CREDENTIAL ADMIN,,,CredentialIssuer,read,,,,,credential-admin,,,allow,filterByUserNS +CREDENTIAL ADMIN,,,CredentialIssuer,read,,,,,credential-admin,,,allow,filterByUserNSorSharedTrue CREDENTIAL ADMIN,,,CredentialIssuer,update,,namespace,,,credential-admin,,,deny, CREDENTIAL ADMIN,,,CredentialIssuer,,"create,read",namespace,,,credential-admin,,,allow, CREDENTIAL ADMIN,,sharedIdPs,,,,,,,credential-admin,,,allow, +CREDENTIAL ADMIN,,allSharedIdPs,,,,,,,credential-admin,,,allow, +CREDENTIAL ADMIN,,allProviderUsers,,,,,,,credential-admin,,,allow, CREDENTIAL ADMIN,,OwnedCredentialIssuer,,,,,,,credential-admin,,,allow, CREDENTIAL ADMIN,,allCredentialIssuersByNamespace,,,,,,,credential-admin,,,allow,filterByUserNS CREDENTIAL ADMIN,,,User,read,,,,,credential-admin,,,allow, diff --git a/src/batch/data-rules.js b/src/batch/data-rules.js index 253d20b3e..2ea16dbfa 100644 --- a/src/batch/data-rules.js +++ b/src/batch/data-rules.js @@ -477,10 +477,10 @@ const metadata = { environmentDetails: { name: 'toString' }, inheritFrom: { name: 'connectOne', - list: 'allCredentialIssuers', + list: 'allSharedIdPs', refKey: 'name', }, - owner: { name: 'connectOne', list: 'allUsers', refKey: 'username' }, + owner: { name: 'connectOne', list: 'allProviderUsers', refKey: 'email' }, }, validations: { isShared: { type: 'boolean' }, @@ -505,7 +505,7 @@ const metadata = { clientAuthenticator: 'client-secret', mode: 'auto', environmentDetails: [], - owner: 'acope@idir', + owner: 'janis@gov.bc.ca', }, }, IssuerEnvironmentConfig: { diff --git a/src/batch/feed-worker.ts b/src/batch/feed-worker.ts index 97bd3c156..c805c7660 100644 --- a/src/batch/feed-worker.ts +++ b/src/batch/feed-worker.ts @@ -335,173 +335,183 @@ export const syncRecords = async function ( buildQueryResponse(md) ); if (localRecord == null) { - const data: any = {}; - for (const field of md.sync) { - if (field in json) { - data[field] = json[field]; + try { + const data: any = {}; + for (const field of md.sync) { + if (field in json) { + data[field] = json[field]; + } } - } - if ('transformations' in md) { - for (const transformKey of Object.keys(md.transformations)) { - const transformInfo = md.transformations[transformKey]; - if (transformInfo.syncFirst) { - // handle these children independently first - return a list of IDs - const allIds = await syncListOfRecords( + if ('transformations' in md) { + for (const transformKey of Object.keys(md.transformations)) { + const transformInfo = md.transformations[transformKey]; + if (transformInfo.syncFirst) { + // handle these children independently first - return a list of IDs + const allIds = await syncListOfRecords( + context, + transformInfo, + json[transformKey] + ); + logger.debug('CHILDREN [%s] %j', transformKey, allIds); + assert.strictEqual( + allIds.filter((record) => record.status != 200).length, + 0, + 'Failed updating children' + ); + assert.strictEqual( + allIds.filter((record) => typeof record.ownedBy != 'undefined') + .length, + 0, + 'There are some child records that have exclusive ownership already!' + ); + json[transformKey + '_ids'] = allIds.map((status) => status.id); + + childResults.push(...allIds); + } + const transformMutation = await transformations[transformInfo.name]( context, transformInfo, - json[transformKey] - ); - logger.debug('CHILDREN [%s] %j', transformKey, allIds); - assert.strictEqual( - allIds.filter((record) => record.status != 200).length, - 0, - 'Failed updating children' - ); - assert.strictEqual( - allIds.filter((record) => typeof record.ownedBy != 'undefined') - .length, - 0, - 'There are some child records that have exclusive ownership already!' + null, + json, + transformKey ); - json[transformKey + '_ids'] = allIds.map((status) => status.id); - - childResults.push(...allIds); - } - const transformMutation = await transformations[transformInfo.name]( - context, - transformInfo, - null, - json, - transformKey - ); - if (transformMutation != null) { - logger.debug( - ' -- Updated [' + - transformKey + - '] ' + - JSON.stringify(data[transformKey]) + - ' to ' + - JSON.stringify(transformMutation) - ); - data[transformKey] = transformMutation; + if (transformMutation != null) { + logger.debug( + ' -- Updated [' + + transformKey + + '] ' + + JSON.stringify(data[transformKey]) + + ' to ' + + JSON.stringify(transformMutation) + ); + data[transformKey] = transformMutation; + } } } - } - data[md.refKey] = eid; - const nr = await batchService.create(entity, data); - if (nr == null) { - logger.error('CREATE FAILED (%s) %j', nr, data); + data[md.refKey] = eid; + const nr = await batchService.create(entity, data); + if (nr == null) { + logger.error('CREATE FAILED (%s) %j', nr, data); + return { status: 400, result: 'create-failed', childResults }; + } else { + return { status: 200, result: 'created', id: nr, childResults }; + } + } catch (ex) { + logger.error('Caught exception %s', ex); return { status: 400, result: 'create-failed', childResults }; - } else { - return { status: 200, result: 'created', id: nr, childResults }; } } else { - const transformKeys = - 'transformations' in md ? Object.keys(md.transformations) : []; - const data: any = {}; - - for (const field of md.sync) { - if (!transformKeys.includes(field)) { - logger.debug( - ' -- changed? (%s) %j -> %j', - field, - localRecord[field], - json[field] - ); - if (field in json && json[field] !== localRecord[field]) { - logger.debug(' -- updated'); - data[field] = json[field]; + try { + const transformKeys = + 'transformations' in md ? Object.keys(md.transformations) : []; + const data: any = {}; + + for (const field of md.sync) { + if (!transformKeys.includes(field)) { + logger.debug( + ' -- changed? (%s) %j -> %j', + field, + localRecord[field], + json[field] + ); + if (field in json && json[field] !== localRecord[field]) { + logger.debug(' -- updated'); + data[field] = json[field]; + } } } - } - if ('transformations' in md) { - for (const transformKey of transformKeys) { - logger.debug(' -- changed trans? (%s)', transformKey); - // unset transformKey from data[] - delete data[transformKey]; - const transformInfo = md.transformations[transformKey]; - if (transformInfo.syncFirst) { - // handle these children independently first - return a list of IDs - const allIds = await syncListOfRecords( + if ('transformations' in md) { + for (const transformKey of transformKeys) { + logger.debug(' -- changed trans? (%s)', transformKey); + // unset transformKey from data[] + delete data[transformKey]; + const transformInfo = md.transformations[transformKey]; + if (transformInfo.syncFirst) { + // handle these children independently first - return a list of IDs + const allIds = await syncListOfRecords( + context, + transformInfo, + json[transformKey] + ); + logger.debug('CHILDREN [%s] %j', transformKey, allIds); + assert.strictEqual( + allIds.filter((record) => record.status != 200).length, + 0, + 'Failed updating children' + ); + logger.debug('%j', localRecord); + assert.strictEqual( + allIds.filter( + (record) => + typeof record.ownedBy != 'undefined' && + record.ownedBy != localRecord.id + ).length, + 0, + 'There are some child records that had ownership already (w/ local record)!' + ); + + json[transformKey + '_ids'] = allIds.map((status) => status.id); + childResults.push(...allIds); + } + + const transformMutation = await transformations[transformInfo.name]( context, transformInfo, - json[transformKey] - ); - logger.debug('CHILDREN [%s] %j', transformKey, allIds); - assert.strictEqual( - allIds.filter((record) => record.status != 200).length, - 0, - 'Failed updating children' + localRecord, + json, + transformKey ); - logger.debug('%j', localRecord); - assert.strictEqual( - allIds.filter( - (record) => - typeof record.ownedBy != 'undefined' && - record.ownedBy != localRecord.id - ).length, - 0, - 'There are some child records that had ownership already (w/ local record)!' - ); - - json[transformKey + '_ids'] = allIds.map((status) => status.id); - childResults.push(...allIds); - } - - const transformMutation = await transformations[transformInfo.name]( - context, - transformInfo, - localRecord, - json, - transformKey - ); - if (transformMutation && transformMutation != null) { - logger.debug( - ' -- updated trans (%s) %j -> %j', - transformKey, - localRecord[transformKey], - transformMutation - ); - data[transformKey] = transformMutation; + if (transformMutation && transformMutation != null) { + logger.debug( + ' -- updated trans (%s) %j -> %j', + transformKey, + localRecord[transformKey], + transformMutation + ); + data[transformKey] = transformMutation; + } } } - } - if (Object.keys(data).length === 0) { - logger.debug('[%s] [%s] no update', entity, localRecord.id); - return { - status: 200, - result: 'no-change', - id: localRecord['id'], - childResults, - ownedBy: - md.ownedBy && localRecord[md.ownedBy] - ? localRecord[md.ownedBy].id - : undefined, - }; - } - logger.info( - '[%s] [%s] keys triggering update %j', - entity, - localRecord.id, - Object.keys(data) - ); - const nr = await batchService.update(entity, localRecord.id, data); - if (nr == null) { - logger.error('UPDATE FAILED (%s) %j', nr, data); + if (Object.keys(data).length === 0) { + logger.debug('[%s] [%s] no update', entity, localRecord.id); + return { + status: 200, + result: 'no-change', + id: localRecord['id'], + childResults, + ownedBy: + md.ownedBy && localRecord[md.ownedBy] + ? localRecord[md.ownedBy].id + : undefined, + }; + } + logger.info( + '[%s] [%s] keys triggering update %j', + entity, + localRecord.id, + Object.keys(data) + ); + const nr = await batchService.update(entity, localRecord.id, data); + if (nr == null) { + logger.error('UPDATE FAILED (%s) %j', nr, data); + return { status: 400, result: 'update-failed', childResults }; + } else { + return { + status: 200, + result: 'updated', + id: nr, + childResults, + ownedBy: + md.ownedBy && localRecord[md.ownedBy] + ? localRecord[md.ownedBy].id + : undefined, + }; + } + } catch (ex) { + logger.error('Caught exception %s', ex); return { status: 400, result: 'update-failed', childResults }; - } else { - return { - status: 200, - result: 'updated', - id: nr, - childResults, - ownedBy: - md.ownedBy && localRecord[md.ownedBy] - ? localRecord[md.ownedBy].id - : undefined, - }; } } }; diff --git a/src/lists/extensions/CredentialIssuerExt.ts b/src/lists/extensions/CredentialIssuerExt.ts index 7da967151..f44463ff1 100644 --- a/src/lists/extensions/CredentialIssuerExt.ts +++ b/src/lists/extensions/CredentialIssuerExt.ts @@ -4,13 +4,16 @@ import { generateEnvDetails, lookupSharedIssuers, } from '../../services/keystone'; -import { CredentialIssuer } from '../../services/keystone/types'; +import { + CredentialIssuer, + CredentialIssuerWhereInput, +} from '../../services/keystone/types'; const typeSharedIssuer = ` type SharedIssuer { id: ID! name: String! - environmentDetails: String! + environmentDetails: String }`; module.exports = { @@ -19,6 +22,33 @@ module.exports = { keystone.extendGraphQLSchema({ types: [{ type: typeSharedIssuer }], queries: [ + { + schema: + 'allSharedIdPs(where: CredentialIssuerWhereInput): [SharedIssuer]', + resolver: async ( + item: any, + args: { where: CredentialIssuerWhereInput }, + context: any, + info: any, + { query, access }: any + ) => { + const issuers: CredentialIssuer[] = await lookupSharedIssuers( + context + ); + + return issuers + .filter( + (issuer) => + !Boolean(args.where?.name) || + issuer.name === args.where.name + ) + .map((issuer) => ({ + id: issuer.id, + name: issuer.name, + })); + }, + access: EnforcementPoint, + }, { schema: 'sharedIdPs(profileName: String): [SharedIssuer]', resolver: async ( diff --git a/src/lists/extensions/UserExt.ts b/src/lists/extensions/UserExt.ts new file mode 100644 index 000000000..76b33e03d --- /dev/null +++ b/src/lists/extensions/UserExt.ts @@ -0,0 +1,42 @@ +const { EnforcementPoint } = require('../../authz/enforcement'); +import { lookupProviderUserByEmail } from '../../services/keystone/user'; +import { kebabCase } from 'lodash'; +import { + generateEnvDetails, + lookupSharedIssuers, +} from '../../services/keystone'; +import { + CredentialIssuer, + CredentialIssuerWhereInput, + User, + UserWhereInput, +} from '../../services/keystone/types'; + +module.exports = { + extensions: [ + (keystone: any) => { + keystone.extendGraphQLSchema({ + queries: [ + { + schema: 'allProviderUsers(where: UserWhereInput): [User]', + resolver: async ( + item: any, + args: { where: UserWhereInput }, + context: any, + info: any, + { query, access }: any + ) => { + const user: User = await lookupProviderUserByEmail( + context, + args.where?.email + ); + + return user ? [user] : []; + }, + access: EnforcementPoint, + }, + ], + }); + }, + ], +}; diff --git a/src/nextapp/components/environment-plugins/templates/jwt-keycloak.ts b/src/nextapp/components/environment-plugins/templates/jwt-keycloak.ts index 9d31d5fb7..2b076c043 100644 --- a/src/nextapp/components/environment-plugins/templates/jwt-keycloak.ts +++ b/src/nextapp/components/environment-plugins/templates/jwt-keycloak.ts @@ -1,4 +1,5 @@ import { CredentialIssuer } from '@/shared/types/query.types'; +import { stringify } from 'querystring'; export default function JwtKeycloak( namespace: string, @@ -8,28 +9,36 @@ export default function JwtKeycloak( if (!issuer || !issuer.environmentDetails) { return ''; } - const envDetails: { environment: string; issuerUrl: string }[] = JSON.parse( - issuer.environmentDetails - ); + const envDetails: { + environment: string; + clientRegistration: string; + clientId: string; + issuerUrl: string; + }[] = JSON.parse(issuer.environmentDetails); const env = envDetails.find((e) => e.environment === envName); if (!env) { return ''; } + const allowedAud = + env.clientRegistration === 'shared-idp' + ? `allowed_aud: ${env.clientId}` + : ''; + return ` plugins: - name: jwt-keycloak tags: [ ns.${namespace} ] enabled: true config: - algorithm: RS256 - well_known_template: ${env.issuerUrl}/.well-known/openid-configuration allowed_iss: - ${env.issuerUrl} + ${allowedAud} run_on_preflight: true iss_key_grace_period: 10 maximum_expiration: 0 + algorithm: RS256 claims_to_verify: - exp uri_param_names: diff --git a/src/nextapp/components/environment-plugins/templates/kong-acl-only.ts b/src/nextapp/components/environment-plugins/templates/kong-acl-only.ts index fe621d1e6..541436f9b 100644 --- a/src/nextapp/components/environment-plugins/templates/kong-acl-only.ts +++ b/src/nextapp/components/environment-plugins/templates/kong-acl-only.ts @@ -5,6 +5,6 @@ export default function KongAclOnly(namespace: string, appId: string): string { tags: [ ns.${namespace} ] config: hide_groups_header: true - allow: [ ${appId} ] + allow: [ "${appId}" ] `; } diff --git a/src/nextapp/components/environment-plugins/templates/kong-api-key-acl.ts b/src/nextapp/components/environment-plugins/templates/kong-api-key-acl.ts index 7834f37d9..ad618b214 100644 --- a/src/nextapp/components/environment-plugins/templates/kong-api-key-acl.ts +++ b/src/nextapp/components/environment-plugins/templates/kong-api-key-acl.ts @@ -16,6 +16,6 @@ export default function KongApiKeyAcl( tags: [ ns.${namespace} ] config: hide_groups_header: true - allow: [ ${appId} ] + allow: [ "${appId}" ] `; } diff --git a/src/nextapp/components/namespace-manager/namespace-manager.tsx b/src/nextapp/components/namespace-manager/namespace-manager.tsx index 970c82768..c846ebd93 100644 --- a/src/nextapp/components/namespace-manager/namespace-manager.tsx +++ b/src/nextapp/components/namespace-manager/namespace-manager.tsx @@ -15,12 +15,12 @@ import { ModalCloseButton, Checkbox, } from '@chakra-ui/react'; -import type { NamespaceData } from '@/shared/types/app.types'; +import { Namespace } from '@/shared/types/query.types'; import ExportReport from './export-report'; interface NamespaceManagerProps { - data: NamespaceData[]; + data: Namespace[]; isOpen: boolean; onClose: () => void; } diff --git a/src/nextapp/components/namespace-menu/namespace-menu.tsx b/src/nextapp/components/namespace-menu/namespace-menu.tsx index 77a52ec20..68a61f7ed 100644 --- a/src/nextapp/components/namespace-menu/namespace-menu.tsx +++ b/src/nextapp/components/namespace-menu/namespace-menu.tsx @@ -5,7 +5,6 @@ import { Menu, MenuButton, MenuDivider, - MenuGroup, MenuItem, MenuList, MenuOptionGroup, @@ -18,10 +17,11 @@ import { FaChevronDown } from 'react-icons/fa'; import { useQueryClient } from 'react-query'; import { gql } from 'graphql-request'; import { restApi, useApi } from '@/shared/services/api'; +import { differenceInDays } from 'date-fns'; +import { Namespace } from '@/shared/types/query.types'; + import NamespaceManager from '../namespace-manager'; import NewNamespace from '../new-namespace'; -import type { NamespaceData } from '@/shared/types/app.types'; -import { differenceInDays } from 'date-fns'; interface NamespaceMenuProps { user: UserData; @@ -46,7 +46,7 @@ const NamespaceMenu: React.FC = ({ const today = new Date(); const handleNamespaceChange = React.useCallback( - (namespace: NamespaceData) => async () => { + (namespace: Namespace) => async () => { toast({ title: `Switching to ${namespace.name} namespace`, status: 'info', @@ -127,10 +127,10 @@ const NamespaceMenu: React.FC = ({ > {differenceInDays(today, new Date(n.orgUpdatedAt)) <= 5 && ( - - New - - )} + + New + + )} {n.name} { /* @ts-ignore */ diff --git a/src/nextapp/shared/types/app.types.ts b/src/nextapp/shared/types/app.types.ts index 3fea3e058..c1b94b4c1 100644 --- a/src/nextapp/shared/types/app.types.ts +++ b/src/nextapp/shared/types/app.types.ts @@ -13,7 +13,7 @@ export interface UserData { } export interface NamespaceData { - id: string; + id?: string; name: string; } diff --git a/src/nextapp/shared/types/query.types.ts b/src/nextapp/shared/types/query.types.ts index 12668a47f..336a1a272 100644 --- a/src/nextapp/shared/types/query.types.ts +++ b/src/nextapp/shared/types/query.types.ts @@ -1504,7 +1504,6 @@ export type CredentialIssuerUpdateInput = { resourceType?: Maybe; resourceAccessScope?: Maybe; apiKeyName?: Maybe; - isShared?: Maybe; environments?: Maybe; }; @@ -5381,7 +5380,7 @@ export type MutationUpdateAuthenticatedTemporaryIdentityArgs = { export type Namespace = { __typename?: 'Namespace'; - id: Scalars['String']; + id?: Maybe; name: Scalars['String']; scopes?: Maybe>>; prodEnvId?: Maybe; @@ -6267,6 +6266,7 @@ export type Query = { getNamespaceConsumerAccess?: Maybe; getConsumerProdEnvAccess?: Maybe; consumerScopesAndRoles?: Maybe; + allSharedIdPs?: Maybe>>; sharedIdPs?: Maybe>>; currentNamespace?: Maybe; allNamespaces?: Maybe>>; @@ -6279,6 +6279,7 @@ export type Query = { getResourceSet?: Maybe; allPermissionTickets?: Maybe>>; getPermissionTicketsForResource?: Maybe>>; + allProviderUsers?: Maybe>>; /** The version of the Keystone application serving this API. */ appVersion?: Maybe; authenticatedTemporaryIdentity?: Maybe; @@ -7032,6 +7033,11 @@ export type QueryConsumerScopesAndRolesArgs = { }; +export type QueryAllSharedIdPsArgs = { + where?: Maybe; +}; + + export type QuerySharedIdPsArgs = { profileName?: Maybe; }; @@ -7089,6 +7095,11 @@ export type QueryGetPermissionTicketsForResourceArgs = { resourceId: Scalars['String']; }; + +export type QueryAllProviderUsersArgs = { + where?: Maybe; +}; + /** A keystone list */ export type ServiceAccess = { __typename?: 'ServiceAccess'; @@ -7316,7 +7327,7 @@ export type SharedIssuer = { __typename?: 'SharedIssuer'; id: Scalars['ID']; name: Scalars['String']; - environmentDetails: Scalars['String']; + environmentDetails?: Maybe; }; export enum SortAccessRequestsBy { diff --git a/src/server.ts b/src/server.ts index 73a4e98dd..cad02ab59 100644 --- a/src/server.ts +++ b/src/server.ts @@ -181,6 +181,7 @@ for (const _list of [ 'UMAPolicy', 'UMAResourceSet', 'UMAPermissionTicket', + 'UserExt', ]) { const list = require('./lists/extensions/' + _list); if ('extensions' in list) { diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index 5080cc879..73f3ef0a8 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -126,10 +126,12 @@ export class KeycloakClientRegistrationService { clientMappers .filter((mapper) => mapper.defaultValue !== '') - .forEach((mapper) => { + .forEach((mapper, index) => { if (mapper.name == 'audience') { logger.debug('[clientRegistration] adding mapper %s', mapper); - body.protocolMappers.push(AudienceMapper(mapper.defaultValue)); + body.protocolMappers.push( + AudienceMapper(`audience-rule-${index + 1}`, mapper.defaultValue) + ); } else { logger.warn( '[clientRegistration] skipping unknown mapper %s', diff --git a/src/services/keycloak/templates/protocol-mappers/audience.ts b/src/services/keycloak/templates/protocol-mappers/audience.ts index 4748a1023..5f0be9da2 100644 --- a/src/services/keycloak/templates/protocol-mappers/audience.ts +++ b/src/services/keycloak/templates/protocol-mappers/audience.ts @@ -1,6 +1,6 @@ -export function AudienceMapper(value: string) { +export function AudienceMapper(name: string, value: string) { return { - name: 'audience', + name, protocol: 'openid-connect', protocolMapper: 'oidc-audience-mapper', consentRequired: false, diff --git a/src/services/keystone/types.ts b/src/services/keystone/types.ts index 12668a47f..336a1a272 100644 --- a/src/services/keystone/types.ts +++ b/src/services/keystone/types.ts @@ -1504,7 +1504,6 @@ export type CredentialIssuerUpdateInput = { resourceType?: Maybe; resourceAccessScope?: Maybe; apiKeyName?: Maybe; - isShared?: Maybe; environments?: Maybe; }; @@ -5381,7 +5380,7 @@ export type MutationUpdateAuthenticatedTemporaryIdentityArgs = { export type Namespace = { __typename?: 'Namespace'; - id: Scalars['String']; + id?: Maybe; name: Scalars['String']; scopes?: Maybe>>; prodEnvId?: Maybe; @@ -6267,6 +6266,7 @@ export type Query = { getNamespaceConsumerAccess?: Maybe; getConsumerProdEnvAccess?: Maybe; consumerScopesAndRoles?: Maybe; + allSharedIdPs?: Maybe>>; sharedIdPs?: Maybe>>; currentNamespace?: Maybe; allNamespaces?: Maybe>>; @@ -6279,6 +6279,7 @@ export type Query = { getResourceSet?: Maybe; allPermissionTickets?: Maybe>>; getPermissionTicketsForResource?: Maybe>>; + allProviderUsers?: Maybe>>; /** The version of the Keystone application serving this API. */ appVersion?: Maybe; authenticatedTemporaryIdentity?: Maybe; @@ -7032,6 +7033,11 @@ export type QueryConsumerScopesAndRolesArgs = { }; +export type QueryAllSharedIdPsArgs = { + where?: Maybe; +}; + + export type QuerySharedIdPsArgs = { profileName?: Maybe; }; @@ -7089,6 +7095,11 @@ export type QueryGetPermissionTicketsForResourceArgs = { resourceId: Scalars['String']; }; + +export type QueryAllProviderUsersArgs = { + where?: Maybe; +}; + /** A keystone list */ export type ServiceAccess = { __typename?: 'ServiceAccess'; @@ -7316,7 +7327,7 @@ export type SharedIssuer = { __typename?: 'SharedIssuer'; id: Scalars['ID']; name: Scalars['String']; - environmentDetails: Scalars['String']; + environmentDetails?: Maybe; }; export enum SortAccessRequestsBy { diff --git a/src/services/keystone/user.ts b/src/services/keystone/user.ts index 840c206f5..a3e6a0a18 100644 --- a/src/services/keystone/user.ts +++ b/src/services/keystone/user.ts @@ -147,6 +147,31 @@ export async function lookupUsersByNamespace( return result.data.usersByNamespace; } +export async function lookupProviderUserByEmail( + context: any, + email: string +): Promise { + const result = await context.executeGraphQL({ + query: `query LookupProviderByEmail($email: String!, $providers: [String]!) { + allUsers(where: { email: $email, provider_in: $providers }) { + id + name + username + email + } + }`, + variables: { + email, + providers: ['idir'], + }, + }); + logger.debug('Query [lookupProviderUserByEmail] result %j', result); + + return !result.errors && result.data.allUsers.length === 1 + ? result.data.allUsers[0] + : null; +} + export async function changeUsername( context: any, userId: string, diff --git a/src/test/integrated/keystonejs/user.ts b/src/test/integrated/keystonejs/user.ts index 8bcb87872..357e1b7e6 100644 --- a/src/test/integrated/keystonejs/user.ts +++ b/src/test/integrated/keystonejs/user.ts @@ -25,6 +25,7 @@ import { recordActivity, recordActivityWithBlob, } from '../../../services/keystone/activity'; +import { lookupProviderUserByEmail } from '../../../services/keystone/user'; (async () => { const keystone = await InitKeystone(); @@ -47,9 +48,11 @@ import { authentication: { item: identity }, }); - const record = await getRecord(ctx, 'User', 'acope@idir'); + // const record = await getRecord(ctx, 'User', 'acope@idir'); - o([record].map((o) => removeEmpty(o))); + // o([record].map((o) => removeEmpty(o))); + const result = await lookupProviderUserByEmail(ctx, 'aidan.cope@gmail.com'); + o(result); await keystone.disconnect(); })(); From 6dfebf16242dfeecc665fba728c1a87fc2fa6a68 Mon Sep 17 00:00:00 2001 From: Joshua Jones Date: Fri, 20 Jan 2023 15:32:50 -0800 Subject: [PATCH 5/5] Sprint 62 UI fixes (#710) * Fix missing values when using tabs to create auth profile * Fix auth form and radio card layout. * Fix type error --- src/mocks/resolvers/products.js | 4 +- .../authentication-form.tsx | 6 +- .../authorization-form.tsx | 6 +- .../authorization-profile-dialog.tsx | 64 +++++++++++++++---- .../client-management.tsx | 8 +-- .../radio-card-group/radio-card-group.tsx | 12 +++- .../radio-card-group/radio-card.tsx | 3 +- 7 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/mocks/resolvers/products.js b/src/mocks/resolvers/products.js index eaaac996e..3f57ee71a 100644 --- a/src/mocks/resolvers/products.js +++ b/src/mocks/resolvers/products.js @@ -286,9 +286,9 @@ const _credentialIssuers = [ inheritFrom: { name: 'Silver Dev Shared IdP', }, - availableScopes: '[]', + availableScopes: '["Namespace.Write"]', clientAuthenticator: 'client-secret', - clientRoles: '[]', + clientRoles: '["read", "write"]', clientMappers: '[{"name":"audience","defaultValue":""}]', apiKeyName: 'X-API-KEY', resourceType: '', diff --git a/src/nextapp/components/authorization-profile-form/authentication-form.tsx b/src/nextapp/components/authorization-profile-form/authentication-form.tsx index 761c692ca..02e9d5079 100644 --- a/src/nextapp/components/authorization-profile-form/authentication-form.tsx +++ b/src/nextapp/components/authorization-profile-form/authentication-form.tsx @@ -4,7 +4,6 @@ import { ButtonGroup, FormControl, FormLabel, - Input, ModalBody, ModalFooter, Radio, @@ -34,6 +33,7 @@ const AuthenticationForm: React.FC = ({ const isKong = value === 'kong-api-key-acl'; const createText = isKong ? 'Create' : 'Continue'; const submitButtonText = id ? 'Save' : createText; + const cancelButtonText = id ? 'Close' : 'Cancel'; // Events const handleCreate = () => { @@ -47,7 +47,7 @@ const AuthenticationForm: React.FC = ({ return ( <> -