From 546c222f373e0abc559164903d0c2761fe34ccfc Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Mon, 12 Feb 2024 14:47:57 -0800 Subject: [PATCH 01/11] move types and run license check --- .../harnesses/adinboundcontrolharness.svg | 19 ++- .../integration/harnesses/esc3harness3.svg | 19 ++- .../integration/harnesses/sessionharness.svg | 19 ++- .../usePermissions/usePermissions.test.tsx | 90 +++++++++++ .../hooks/usePermissions/usePermissions.tsx | 46 ++++++ cmd/ui/src/test-utils.jsx | 144 +++++++++++------- .../bh-shared-ui/src/utils/index.ts | 1 + .../bh-shared-ui/src/utils/permissions.ts | 50 ++++++ 8 files changed, 327 insertions(+), 61 deletions(-) create mode 100644 cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx create mode 100644 cmd/ui/src/hooks/usePermissions/usePermissions.tsx create mode 100644 packages/javascript/bh-shared-ui/src/utils/permissions.ts diff --git a/cmd/api/src/test/integration/harnesses/adinboundcontrolharness.svg b/cmd/api/src/test/integration/harnesses/adinboundcontrolharness.svg index a496e2ade4..3f36947d85 100644 --- a/cmd/api/src/test/integration/harnesses/adinboundcontrolharness.svg +++ b/cmd/api/src/test/integration/harnesses/adinboundcontrolharness.svg @@ -1 +1,18 @@ -GenericAllMemberOfGenericAllGenericAllMemberOfGenericAllGenericWriteWriteOwnerWriteDaclGenericWriteWriteDaclOwnsWriteOwnerGenericWriteGenericWriteMemberOfMemberOfMemberOfControlledUserGroupBUserAUserDUserCGroupAUserBControlledGroupGroupCGroupDUserFUserEUserGUserH \ No newline at end of file + +GenericAllMemberOfGenericAllGenericAllMemberOfGenericAllGenericWriteWriteOwnerWriteDaclGenericWriteWriteDaclOwnsWriteOwnerGenericWriteGenericWriteMemberOfMemberOfMemberOfControlledUserGroupBUserAUserDUserCGroupAUserBControlledGroupGroupCGroupDUserFUserEUserGUserH diff --git a/cmd/api/src/test/integration/harnesses/esc3harness3.svg b/cmd/api/src/test/integration/harnesses/esc3harness3.svg index cdc713de65..a6846efd10 100644 --- a/cmd/api/src/test/integration/harnesses/esc3harness3.svg +++ b/cmd/api/src/test/integration/harnesses/esc3harness3.svg @@ -1 +1,18 @@ -RootCAForIssuedSignedByNTAuthStoreForTrustedForNTAuthPublishedToPublishedToEnrollOnBehalfOfEnrollMemberOfAllExtendedRightsEnrollDomainNTAuthStoreRootCAEnterpriseCA1EnrollmentAgentRestrictionsCollected:FalseCertTemplate2AuthenticationEnabled:TrueRequiresManagerApproval:FalseSubjectAltRequireUPN:TrueCertTemplate1RequiresManagerApproval:FalseAuthorizedSignatures:0SchemaVersion:2Group1User2 \ No newline at end of file + +RootCAForIssuedSignedByNTAuthStoreForTrustedForNTAuthPublishedToPublishedToEnrollOnBehalfOfEnrollMemberOfAllExtendedRightsEnrollDomainNTAuthStoreRootCAEnterpriseCA1EnrollmentAgentRestrictionsCollected:FalseCertTemplate2AuthenticationEnabled:TrueRequiresManagerApproval:FalseSubjectAltRequireUPN:TrueCertTemplate1RequiresManagerApproval:FalseAuthorizedSignatures:0SchemaVersion:2Group1User2 diff --git a/cmd/api/src/test/integration/harnesses/sessionharness.svg b/cmd/api/src/test/integration/harnesses/sessionharness.svg index 3b84d62406..76016b9fff 100644 --- a/cmd/api/src/test/integration/harnesses/sessionharness.svg +++ b/cmd/api/src/test/integration/harnesses/sessionharness.svg @@ -1 +1,18 @@ -HasSessionMemberOfHasSessionHasSessionMemberOfHasSessionMemberOfMemberOfMemberOfHasSessionMemberOfComputerAUserAGroupAComputerBComputerAUserAGroupBComputerBGroupCComputerCUserB \ No newline at end of file + +HasSessionMemberOfHasSessionHasSessionMemberOfHasSessionMemberOfMemberOfMemberOfHasSessionMemberOfComputerAUserAGroupAComputerBComputerAUserAGroupBComputerBGroupCComputerCUserB diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx new file mode 100644 index 0000000000..affb6b3550 --- /dev/null +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -0,0 +1,90 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { PermissionsAuthority, PermissionsName, PermissionsSpec } from 'bh-shared-ui'; +import { renderHook } from 'src/test-utils'; +import usePermissions from './usePermissions'; + +describe('usePermissions', () => { + const checkPermissions = (permissions: { has: PermissionsSpec[]; needs: PermissionsSpec[] }) => { + return renderHook(() => usePermissions(permissions.needs), { + initialState: { + auth: { + user: { + roles: [ + { + permissions: permissions.has, + }, + ], + }, + }, + }, + }).result.current; + }; + + const manageClientsPermission = { + authority: PermissionsAuthority.CLIENTS, + name: PermissionsName.MANAGE_CLIENTS, + }; + + const createTokenPermission = { + authority: PermissionsAuthority.AUTH, + name: PermissionsName.CREATE_TOKEN, + }; + + const manageAppConfigPermission = { + authority: PermissionsAuthority.APP, + name: PermissionsName.MANAGE_APP_CONFIG, + }; + + const allPermissions = [manageClientsPermission, createTokenPermission, manageAppConfigPermission]; + + it('returns true if the user has a required permission', () => { + const hasPermissions = checkPermissions({ + has: [manageClientsPermission], + needs: [manageClientsPermission], + }); + + expect(hasPermissions).toBe(true); + }); + + it('returns true if the user has multiple required permissions', () => { + const hasPermissions = checkPermissions({ + has: allPermissions, + needs: allPermissions, + }); + + expect(hasPermissions).toBe(true); + }); + + it('returns false if the user does not have a matching permission', () => { + const hasPermissions = checkPermissions({ + has: [manageClientsPermission], + needs: [createTokenPermission], + }); + + expect(hasPermissions).toBe(false); + }); + + it('returns false if the user is missing one of many required permissions', () => { + const hasPermissions = checkPermissions({ + has: [manageClientsPermission, createTokenPermission], + needs: allPermissions, + }); + + expect(hasPermissions).toBe(false); + }); +}); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx new file mode 100644 index 0000000000..5d44ce94bf --- /dev/null +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -0,0 +1,46 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { getSelfResponse } from 'src/ducks/auth/types'; +import { PermissionsSpec } from 'bh-shared-ui'; + +const usePermissions: (permissions: PermissionsSpec[]) => boolean = (permissions) => { + const [hasAllPermissions, setHasAllPermissions] = useState(false); + const auth: { user: getSelfResponse } = useSelector((state: any) => state.auth); + + const doesUserHavePermissions = useCallback( + (user: getSelfResponse): boolean => { + const userPermissions = user.roles.map((role) => role.permissions).flat(); + + return permissions.every((permission) => { + return userPermissions.some((userPermission) => { + return userPermission.authority === permission.authority && userPermission.name === permission.name; + }); + }); + }, + [permissions] + ); + + useEffect(() => { + setHasAllPermissions(auth.user && doesUserHavePermissions(auth.user)); + }, [auth, doesUserHavePermissions]); + + return hasAllPermissions; +}; + +export default usePermissions; diff --git a/cmd/ui/src/test-utils.jsx b/cmd/ui/src/test-utils.jsx index dd44839f24..4d046a5c3f 100644 --- a/cmd/ui/src/test-utils.jsx +++ b/cmd/ui/src/test-utils.jsx @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import { createTheme } from '@mui/material/styles'; import { CssBaseline, StyledEngineProvider, ThemeProvider } from '@mui/material'; import { configureStore } from '@reduxjs/toolkit'; -import { render } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { SnackbarProvider } from 'notistack'; import { QueryClient, QueryClientProvider } from 'react-query'; @@ -27,75 +27,103 @@ import createSagaMiddleware from 'redux-saga'; import { rootReducer } from 'src/store'; import { NotificationsProvider } from 'bh-shared-ui'; +const defaultTheme = { + palette: { + primary: { + main: '#406f8e', + light: '#709dbe', + dark: '#064460', + contrastText: '#ffffff', + }, + neutral: { + main: '#e0e0e0', + light: '#ffffff', + dark: '#cccccc', + contrastText: '#000000', + }, + background: { + paper: '#fafafa', + default: '#e4e9eb', + }, + low: 'rgb(255, 195, 15)', + moderate: 'rgb(255, 97, 66)', + high: 'rgb(205, 0, 117)', + critical: 'rgb(76, 29, 143)', + }, +} + +const createDefaultQueryClient = () => { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) +} + +const createDefaultStore = (state) => { + return configureStore({ + reducer: rootReducer, + preloadedState: state, + middleware: (getDefaultMiddleware) => { + return [...getDefaultMiddleware({ serializableCheck: false }), createSagaMiddleware()]; + }, + }) +} + +const createProviders = ({ queryClient, history, theme, store, children }) => { + return ( + + + + + + + + {children} + + + + + + + ) +} + const customRender = ( ui, { initialState = {}, - queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }), + queryClient = createDefaultQueryClient(), history = createMemoryHistory(), - theme = createTheme({ - palette: { - primary: { - main: '#406f8e', - light: '#709dbe', - dark: '#064460', - contrastText: '#ffffff', - }, - neutral: { - main: '#e0e0e0', - light: '#ffffff', - dark: '#cccccc', - contrastText: '#000000', - }, - background: { - paper: '#fafafa', - default: '#e4e9eb', - }, - low: 'rgb(255, 195, 15)', - moderate: 'rgb(255, 97, 66)', - high: 'rgb(205, 0, 117)', - critical: 'rgb(76, 29, 143)', - }, - }), - store = configureStore({ - reducer: rootReducer, - preloadedState: initialState, - middleware: (getDefaultMiddleware) => { - return [...getDefaultMiddleware({ serializableCheck: false }), createSagaMiddleware()]; - }, - }), + theme = createTheme(defaultTheme), + store = createDefaultStore(initialState), ...renderOptions } = {} ) => { - const AllTheProviders = ({ children }) => { - return ( - - - - - - - - {children} - - - - - - - ); - }; + const AllTheProviders = ({ children }) => createProviders({ queryClient, history, theme, store, children }); return render(ui, { wrapper: AllTheProviders, ...renderOptions }); }; +const customRenderHook = ( + hook, + { + initialState = {}, + queryClient = createDefaultQueryClient(), + history = createMemoryHistory(), + theme = createTheme(defaultTheme), + store = createDefaultStore(initialState), + ...renderOptions + } = {} +) => { + const AllTheProviders = ({ children }) => createProviders({ queryClient, history, theme, store, children }); + return renderHook(hook, { wrapper: AllTheProviders, ...renderOptions }); +}; + // re-export everything // eslint-disable-next-line react-refresh/only-export-components export * from '@testing-library/react'; // override render method export { customRender as render }; +export { customRenderHook as renderHook }; diff --git a/packages/javascript/bh-shared-ui/src/utils/index.ts b/packages/javascript/bh-shared-ui/src/utils/index.ts index ecadd25b65..6c58c14662 100644 --- a/packages/javascript/bh-shared-ui/src/utils/index.ts +++ b/packages/javascript/bh-shared-ui/src/utils/index.ts @@ -22,3 +22,4 @@ export * from './entityInfoDisplay'; export * from './passwd'; export * from './user'; export * from './icons'; +export * from './permissions'; diff --git a/packages/javascript/bh-shared-ui/src/utils/permissions.ts b/packages/javascript/bh-shared-ui/src/utils/permissions.ts new file mode 100644 index 0000000000..0c4d920f75 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/utils/permissions.ts @@ -0,0 +1,50 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +export enum PermissionsAuthority { + APP = 'app', + RISKS = 'risks', + AUTH = 'auth', + CLIENTS = 'clients', + COLLECTION = 'collection', + GRAPHDB = 'graphdb', + SAVED_QUERIES = 'saved_queries', +} + +export enum PermissionsName { + READ_APP_CONFIG = 'ReadAppConfig', + WRITE_APP_CONFIG = 'WriteAppConfig', + GENERATE_REPORT = 'GenerateReport', + MANAGE_RISKS = 'ManageRisks', + CREATE_TOKEN = 'CreateToken', + MANAGE_APP_CONFIG = 'ManageAppConfig', + MANAGE_PROVIDERS = 'ManageProviders', + MANAGE_SELF = 'ManageSelf', + MANAGE_USERS = 'ManageUsers', + MANAGE_CLIENTS = 'Manage', + READ_CLIENTS = 'Read', + CLIENT_TASKING = 'Tasking', + MANAGE_COLLECTION_JOBS = 'ManageJobs', + READ_GRAPHDB = 'Read', + WRITE_GRAPHDB = 'Write', + READ_SAVED_QUERIES = 'Read', + WRITE_SAVED_QUERIES = 'Write', +} + +export type PermissionsSpec = { + authority: PermissionsAuthority; + name: PermissionsName; +}; From 5d16e3cbfba0ff213dcc944c9faf6104932394c3 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Wed, 14 Feb 2024 13:30:03 -0800 Subject: [PATCH 02/11] pulling in most recent main --- .../usePermissions/usePermissions.test.tsx | 44 ++++++---------- .../hooks/usePermissions/usePermissions.tsx | 51 +++++++++---------- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index affb6b3550..c152ed247d 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -1,19 +1,3 @@ -// Copyright 2024 Specter Ops, Inc. -// -// Licensed under the Apache License, Version 2.0 -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - import { PermissionsAuthority, PermissionsName, PermissionsSpec } from 'bh-shared-ui'; import { renderHook } from 'src/test-utils'; import usePermissions from './usePermissions'; @@ -52,39 +36,43 @@ describe('usePermissions', () => { const allPermissions = [manageClientsPermission, createTokenPermission, manageAppConfigPermission]; - it('returns true if the user has a required permission', () => { - const hasPermissions = checkPermissions({ + it('permitted if the user has a required permission', () => { + const permissions = checkPermissions({ has: [manageClientsPermission], needs: [manageClientsPermission], }); - expect(hasPermissions).toBe(true); + expect(permissions.hasAll).toBe(true); + expect(permissions.hasAtLeastOne).toBe(true); }); - it('returns true if the user has multiple required permissions', () => { - const hasPermissions = checkPermissions({ + it('permitted if the user has multiple required permissions', () => { + const permissions = checkPermissions({ has: allPermissions, needs: allPermissions, }); - expect(hasPermissions).toBe(true); + expect(permissions.hasAll).toBe(true); + expect(permissions.hasAtLeastOne).toBe(true); }); - it('returns false if the user does not have a matching permission', () => { - const hasPermissions = checkPermissions({ + it('denied if the user does not have a matching permission', () => { + const permissions = checkPermissions({ has: [manageClientsPermission], needs: [createTokenPermission], }); - expect(hasPermissions).toBe(false); + expect(permissions.hasAll).toBe(false); + expect(permissions.hasAtLeastOne).toBe(false); }); - it('returns false if the user is missing one of many required permissions', () => { - const hasPermissions = checkPermissions({ + it('returns hasAtLeastOne if the user is missing one of many required permissions', () => { + const permissions = checkPermissions({ has: [manageClientsPermission, createTokenPermission], needs: allPermissions, }); - expect(hasPermissions).toBe(false); + expect(permissions.hasAll).toBe(false); + expect(permissions.hasAtLeastOne).toBe(true); }); }); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 5d44ce94bf..1859d46339 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -1,46 +1,45 @@ -// Copyright 2024 Specter Ops, Inc. -// -// Licensed under the Apache License, Version 2.0 -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { getSelfResponse } from 'src/ducks/auth/types'; import { PermissionsSpec } from 'bh-shared-ui'; +import { AppState } from 'src/store'; + +type PermissionState = { + hasAtLeastOne: boolean; + hasAll: boolean; +}; -const usePermissions: (permissions: PermissionsSpec[]) => boolean = (permissions) => { - const [hasAllPermissions, setHasAllPermissions] = useState(false); - const auth: { user: getSelfResponse } = useSelector((state: any) => state.auth); +const usePermissions: (permissions: PermissionsSpec[]) => PermissionState = (permissions) => { + const [permissionState, setPermissionState] = useState({ hasAtLeastOne: false, hasAll: false }); + const auth = useSelector((state: AppState) => state.auth); - const doesUserHavePermissions = useCallback( - (user: getSelfResponse): boolean => { + const checkUserPermissions = useCallback( + (user: getSelfResponse): PermissionState => { const userPermissions = user.roles.map((role) => role.permissions).flat(); + let hasAtLeastOne = false; - return permissions.every((permission) => { + const hasAll = permissions.every((permission) => { return userPermissions.some((userPermission) => { - return userPermission.authority === permission.authority && userPermission.name === permission.name; + const matched = + userPermission.authority === permission.authority && userPermission.name === permission.name; + + if (matched && !hasAtLeastOne) hasAtLeastOne = true; + return matched; }); }); + + return { hasAtLeastOne, hasAll }; }, [permissions] ); useEffect(() => { - setHasAllPermissions(auth.user && doesUserHavePermissions(auth.user)); - }, [auth, doesUserHavePermissions]); + if (auth.user) { + setPermissionState(checkUserPermissions(auth.user)); + } + }, [auth, checkUserPermissions]); - return hasAllPermissions; + return permissionState; }; export default usePermissions; From d69b6e49c64a448847278389657de9a716817514 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Wed, 14 Feb 2024 13:39:12 -0800 Subject: [PATCH 03/11] added useAppSelector --- cmd/ui/src/hooks/usePermissions/usePermissions.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 1859d46339..6a68397322 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -1,8 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import { getSelfResponse } from 'src/ducks/auth/types'; import { PermissionsSpec } from 'bh-shared-ui'; -import { AppState } from 'src/store'; +import { useAppSelector } from 'src/store'; type PermissionState = { hasAtLeastOne: boolean; @@ -11,7 +10,7 @@ type PermissionState = { const usePermissions: (permissions: PermissionsSpec[]) => PermissionState = (permissions) => { const [permissionState, setPermissionState] = useState({ hasAtLeastOne: false, hasAll: false }); - const auth = useSelector((state: AppState) => state.auth); + const auth = useAppSelector((state) => state.auth); const checkUserPermissions = useCallback( (user: getSelfResponse): PermissionState => { From d71499947dfae83437c1df68a4e776038a53bca5 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Wed, 14 Feb 2024 13:40:12 -0800 Subject: [PATCH 04/11] ran license check --- .../hooks/usePermissions/usePermissions.test.tsx | 16 ++++++++++++++++ .../src/hooks/usePermissions/usePermissions.tsx | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index c152ed247d..f964028541 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -1,3 +1,19 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + import { PermissionsAuthority, PermissionsName, PermissionsSpec } from 'bh-shared-ui'; import { renderHook } from 'src/test-utils'; import usePermissions from './usePermissions'; diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 6a68397322..cd4203fa44 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -1,3 +1,19 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + import { useCallback, useEffect, useState } from 'react'; import { getSelfResponse } from 'src/ducks/auth/types'; import { PermissionsSpec } from 'bh-shared-ui'; From b6f640e9d343c0295a8bd2097a208a40a50dd6d4 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Thu, 15 Feb 2024 11:54:23 -0800 Subject: [PATCH 05/11] updated from PR feedback --- .../usePermissions/usePermissions.test.tsx | 2 +- .../hooks/usePermissions/usePermissions.tsx | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index f964028541..460fee0951 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -84,7 +84,7 @@ describe('usePermissions', () => { it('returns hasAtLeastOne if the user is missing one of many required permissions', () => { const permissions = checkPermissions({ - has: [manageClientsPermission, createTokenPermission], + has: [manageAppConfigPermission, createTokenPermission], needs: allPermissions, }); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index cd4203fa44..58efdbab03 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -31,17 +31,24 @@ const usePermissions: (permissions: PermissionsSpec[]) => PermissionState = (per const checkUserPermissions = useCallback( (user: getSelfResponse): PermissionState => { const userPermissions = user.roles.map((role) => role.permissions).flat(); + + const userPermMap: Record = {}; + const getPermKey = (authority: string, name: string): string => `${authority}-${name}`; + + userPermissions.forEach((perm) => (userPermMap[getPermKey(perm.authority, perm.name)] = true)); + + let hasAll = true; let hasAtLeastOne = false; - const hasAll = permissions.every((permission) => { - return userPermissions.some((userPermission) => { - const matched = - userPermission.authority === permission.authority && userPermission.name === permission.name; + for (const perm of permissions) { + const match = userPermMap[getPermKey(perm.authority, perm.name)]; - if (matched && !hasAtLeastOne) hasAtLeastOne = true; - return matched; - }); - }); + if (match) { + hasAtLeastOne = true; + } else { + hasAll = false; + } + } return { hasAtLeastOne, hasAll }; }, From ce6409539a4590c8dbd482aa1eb7b826181227d6 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Fri, 16 Feb 2024 13:41:16 -0800 Subject: [PATCH 06/11] rewrote hook and permission structure based on feedback --- .../usePermissions/usePermissions.test.tsx | 82 +++++++++---------- .../hooks/usePermissions/usePermissions.tsx | 65 +++++++-------- .../bh-shared-ui/src/utils/permissions.ts | 67 +++++++-------- 3 files changed, 101 insertions(+), 113 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index 460fee0951..a672487da6 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -14,19 +14,19 @@ // // SPDX-License-Identifier: Apache-2.0 -import { PermissionsAuthority, PermissionsName, PermissionsSpec } from 'bh-shared-ui'; +import { Permission } from 'bh-shared-ui'; import { renderHook } from 'src/test-utils'; -import usePermissions from './usePermissions'; +import usePermissions, { PermissionsFns } from './usePermissions'; describe('usePermissions', () => { - const checkPermissions = (permissions: { has: PermissionsSpec[]; needs: PermissionsSpec[] }) => { - return renderHook(() => usePermissions(permissions.needs), { + const getPermissionsWithUser = (permissions: Permission[]): PermissionsFns => { + return renderHook(() => usePermissions(), { initialState: { auth: { user: { roles: [ { - permissions: permissions.has, + permissions: permissions.map((p) => p.get()), }, ], }, @@ -35,60 +35,52 @@ describe('usePermissions', () => { }).result.current; }; - const manageClientsPermission = { - authority: PermissionsAuthority.CLIENTS, - name: PermissionsName.MANAGE_CLIENTS, - }; - - const createTokenPermission = { - authority: PermissionsAuthority.AUTH, - name: PermissionsName.CREATE_TOKEN, - }; - - const manageAppConfigPermission = { - authority: PermissionsAuthority.APP, - name: PermissionsName.MANAGE_APP_CONFIG, - }; - - const allPermissions = [manageClientsPermission, createTokenPermission, manageAppConfigPermission]; + const allPermissions = [ + Permission.CLIENTS_MANAGE, + Permission.AUTH_CREATE_TOKEN, + Permission.APP_READ_APPLICATION_CONFIGURATION, + ]; it('permitted if the user has a required permission', () => { - const permissions = checkPermissions({ - has: [manageClientsPermission], - needs: [manageClientsPermission], - }); + const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); + + const hasAll = permissions.checkAllPermissions([Permission.CLIENTS_MANAGE]); + const hasAtLeastOne = permissions.checkAtLeastOnePermission([Permission.CLIENTS_MANAGE]); - expect(permissions.hasAll).toBe(true); - expect(permissions.hasAtLeastOne).toBe(true); + expect(hasAll).toBe(true); + expect(hasAtLeastOne).toBe(true); }); it('permitted if the user has multiple required permissions', () => { - const permissions = checkPermissions({ - has: allPermissions, - needs: allPermissions, - }); + const permissions = getPermissionsWithUser(allPermissions); + + const hasAll = permissions.checkAllPermissions(allPermissions); + const hasAtLeastOne = permissions.checkAtLeastOnePermission(allPermissions); - expect(permissions.hasAll).toBe(true); - expect(permissions.hasAtLeastOne).toBe(true); + expect(hasAll).toBe(true); + expect(hasAtLeastOne).toBe(true); }); it('denied if the user does not have a matching permission', () => { - const permissions = checkPermissions({ - has: [manageClientsPermission], - needs: [createTokenPermission], - }); + const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); - expect(permissions.hasAll).toBe(false); - expect(permissions.hasAtLeastOne).toBe(false); + const hasAll = permissions.checkAllPermissions([Permission.AUTH_CREATE_TOKEN]); + const hasAtLeastOne = permissions.checkAtLeastOnePermission([Permission.AUTH_CREATE_TOKEN]); + + expect(hasAll).toBe(false); + expect(hasAtLeastOne).toBe(false); }); it('returns hasAtLeastOne if the user is missing one of many required permissions', () => { - const permissions = checkPermissions({ - has: [manageAppConfigPermission, createTokenPermission], - needs: allPermissions, - }); + const permissions = getPermissionsWithUser([ + Permission.APP_READ_APPLICATION_CONFIGURATION, + Permission.AUTH_CREATE_TOKEN, + ]); + + const hasAll = permissions.checkAllPermissions(allPermissions); + const hasAtLeastOne = permissions.checkAtLeastOnePermission(allPermissions); - expect(permissions.hasAll).toBe(false); - expect(permissions.hasAtLeastOne).toBe(true); + expect(hasAll).toBe(false); + expect(hasAtLeastOne).toBe(true); }); }); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 58efdbab03..d8f54fb68c 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -14,54 +14,47 @@ // // SPDX-License-Identifier: Apache-2.0 +import { Permission } from 'bh-shared-ui'; import { useCallback, useEffect, useState } from 'react'; -import { getSelfResponse } from 'src/ducks/auth/types'; -import { PermissionsSpec } from 'bh-shared-ui'; import { useAppSelector } from 'src/store'; -type PermissionState = { - hasAtLeastOne: boolean; - hasAll: boolean; +export type PermissionsFns = { + checkAllPermissions: (permissions: Permission[]) => boolean; + checkAtLeastOnePermission: (permissions: Permission[]) => boolean; }; -const usePermissions: (permissions: PermissionsSpec[]) => PermissionState = (permissions) => { - const [permissionState, setPermissionState] = useState({ hasAtLeastOne: false, hasAll: false }); +const usePermissions = () => { const auth = useAppSelector((state) => state.auth); + const [userPermMap, setUserPermMap] = useState>({}); - const checkUserPermissions = useCallback( - (user: getSelfResponse): PermissionState => { - const userPermissions = user.roles.map((role) => role.permissions).flat(); + const formatKey = useCallback((perm: { authority: string; name: string }) => `${perm.authority}-${perm.name}`, []); - const userPermMap: Record = {}; - const getPermKey = (authority: string, name: string): string => `${authority}-${name}`; - - userPermissions.forEach((perm) => (userPermMap[getPermKey(perm.authority, perm.name)] = true)); - - let hasAll = true; - let hasAtLeastOne = false; - - for (const perm of permissions) { - const match = userPermMap[getPermKey(perm.authority, perm.name)]; + useEffect(() => { + const userPermissions = auth.user?.roles.map((role) => role.permissions).flat(); - if (match) { - hasAtLeastOne = true; - } else { - hasAll = false; - } - } + if (userPermissions) { + const newPermMap: Record = {}; + userPermissions.forEach((perm) => (newPermMap[formatKey(perm)] = true)); + setUserPermMap(newPermMap); + } + }, [auth.user, formatKey]); - return { hasAtLeastOne, hasAll }; - }, - [permissions] - ); + const checkAllPermissions = (permissions: Permission[]): boolean => { + for (const perm of permissions) { + const match = userPermMap[formatKey(perm.get())]; + if (!match) return false; + } + return true; + }; - useEffect(() => { - if (auth.user) { - setPermissionState(checkUserPermissions(auth.user)); + const checkAtLeastOnePermission = (permissions: Permission[]): boolean => { + for (const perm of permissions) { + const match = userPermMap[formatKey(perm.get())]; + if (match) return true; } - }, [auth, checkUserPermissions]); + return false; + }; - return permissionState; + return { checkAllPermissions, checkAtLeastOnePermission }; }; - export default usePermissions; diff --git a/packages/javascript/bh-shared-ui/src/utils/permissions.ts b/packages/javascript/bh-shared-ui/src/utils/permissions.ts index 0c4d920f75..1a7af44c1a 100644 --- a/packages/javascript/bh-shared-ui/src/utils/permissions.ts +++ b/packages/javascript/bh-shared-ui/src/utils/permissions.ts @@ -13,38 +13,41 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +export class Permission { + private constructor(private readonly authority: string, private readonly name: string) { + this.name = name; + this.authority = authority; + } -export enum PermissionsAuthority { - APP = 'app', - RISKS = 'risks', - AUTH = 'auth', - CLIENTS = 'clients', - COLLECTION = 'collection', - GRAPHDB = 'graphdb', - SAVED_QUERIES = 'saved_queries', -} + get() { + return { + name: this.name, + authority: this.authority, + }; + } -export enum PermissionsName { - READ_APP_CONFIG = 'ReadAppConfig', - WRITE_APP_CONFIG = 'WriteAppConfig', - GENERATE_REPORT = 'GenerateReport', - MANAGE_RISKS = 'ManageRisks', - CREATE_TOKEN = 'CreateToken', - MANAGE_APP_CONFIG = 'ManageAppConfig', - MANAGE_PROVIDERS = 'ManageProviders', - MANAGE_SELF = 'ManageSelf', - MANAGE_USERS = 'ManageUsers', - MANAGE_CLIENTS = 'Manage', - READ_CLIENTS = 'Read', - CLIENT_TASKING = 'Tasking', - MANAGE_COLLECTION_JOBS = 'ManageJobs', - READ_GRAPHDB = 'Read', - WRITE_GRAPHDB = 'Write', - READ_SAVED_QUERIES = 'Read', - WRITE_SAVED_QUERIES = 'Write', -} + static readonly APP_READ_APPLICATION_CONFIGURATION = new Permission('app', 'ReadAppConfig'); + static readonly APP_WRITE_APPLICATION_CONFIGURATION = new Permission('app', 'WriteAppConfig'); + + static readonly APS_GENERATE_REPORT = new Permission('risks', 'GenerateReport'); + static readonly APS_MANAGE_APS = new Permission('risks', 'ManageRisks'); + + static readonly AUTH_ACCEPT_EULA = new Permission('auth', 'AcceptEULA'); + static readonly AUTH_CREATE_TOKEN = new Permission('auth', 'CreateToken'); + static readonly AUTH_MANAGE_APPLICATION_CONFIGURATIONS = new Permission('auth', 'ManageAppConfig'); + static readonly AUTH_MANAGE_PROVIDERS = new Permission('auth', 'ManageProviders'); + static readonly AUTH_MANAGE_SELF = new Permission('auth', 'ManageSelf'); + static readonly AUTH_MANAGE_USERS = new Permission('auth', 'ManageUsers'); -export type PermissionsSpec = { - authority: PermissionsAuthority; - name: PermissionsName; -}; + static readonly CLIENTS_MANAGE = new Permission('clients', 'Manage'); + static readonly CLIENTS_READ = new Permission('clients', 'Read'); + static readonly CLIENTS_TASKING = new Permission('clients', 'Tasking'); + + static readonly COLLECTION_MANAGE_JOBS = new Permission('collection', 'ManageJobs'); + + static readonly GRAPH_DB_READ = new Permission('graphdb', 'Read'); + static readonly GRAPH_DB_WRITE = new Permission('graphdb', 'Write'); + + static readonly SAVED_QUERIES_READ = new Permission('saved_queries', 'Read'); + static readonly SAVED_QUERIES_WRITE = new Permission('saved_queries', 'Write'); +} From 87ca3c555e7bae012248efbf4d5fa109c70822e7 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Fri, 16 Feb 2024 13:52:13 -0800 Subject: [PATCH 07/11] added check for single permission --- cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx | 4 ++++ cmd/ui/src/hooks/usePermissions/usePermissions.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index a672487da6..03e42679e0 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -44,9 +44,11 @@ describe('usePermissions', () => { it('permitted if the user has a required permission', () => { const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); + const has = permissions.checkPermission(Permission.CLIENTS_MANAGE); const hasAll = permissions.checkAllPermissions([Permission.CLIENTS_MANAGE]); const hasAtLeastOne = permissions.checkAtLeastOnePermission([Permission.CLIENTS_MANAGE]); + expect(has).toBe(true); expect(hasAll).toBe(true); expect(hasAtLeastOne).toBe(true); }); @@ -64,9 +66,11 @@ describe('usePermissions', () => { it('denied if the user does not have a matching permission', () => { const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); + const has = permissions.checkPermission(Permission.AUTH_CREATE_TOKEN); const hasAll = permissions.checkAllPermissions([Permission.AUTH_CREATE_TOKEN]); const hasAtLeastOne = permissions.checkAtLeastOnePermission([Permission.AUTH_CREATE_TOKEN]); + expect(has).toBe(false); expect(hasAll).toBe(false); expect(hasAtLeastOne).toBe(false); }); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index d8f54fb68c..13a46e04c1 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -19,6 +19,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useAppSelector } from 'src/store'; export type PermissionsFns = { + checkPermission: (permission: Permission) => boolean; checkAllPermissions: (permissions: Permission[]) => boolean; checkAtLeastOnePermission: (permissions: Permission[]) => boolean; }; @@ -39,6 +40,10 @@ const usePermissions = () => { } }, [auth.user, formatKey]); + const checkPermission = (permission: Permission): boolean => { + return !!userPermMap[formatKey(permission.get())]; + }; + const checkAllPermissions = (permissions: Permission[]): boolean => { for (const perm of permissions) { const match = userPermMap[formatKey(perm.get())]; @@ -55,6 +60,6 @@ const usePermissions = () => { return false; }; - return { checkAllPermissions, checkAtLeastOnePermission }; + return { checkPermission, checkAllPermissions, checkAtLeastOnePermission }; }; export default usePermissions; From 31340998ccc65f9e1d8987c55057f9496bb67d89 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Fri, 16 Feb 2024 13:57:47 -0800 Subject: [PATCH 08/11] persistent arg names --- cmd/ui/src/hooks/usePermissions/usePermissions.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 13a46e04c1..d609e589b0 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -28,7 +28,7 @@ const usePermissions = () => { const auth = useAppSelector((state) => state.auth); const [userPermMap, setUserPermMap] = useState>({}); - const formatKey = useCallback((perm: { authority: string; name: string }) => `${perm.authority}-${perm.name}`, []); + const formatKey = useCallback((p: { authority: string; name: string }) => `${p.authority}-${p.name}`, []); useEffect(() => { const userPermissions = auth.user?.roles.map((role) => role.permissions).flat(); @@ -45,16 +45,16 @@ const usePermissions = () => { }; const checkAllPermissions = (permissions: Permission[]): boolean => { - for (const perm of permissions) { - const match = userPermMap[formatKey(perm.get())]; + for (const permission of permissions) { + const match = userPermMap[formatKey(permission.get())]; if (!match) return false; } return true; }; const checkAtLeastOnePermission = (permissions: Permission[]): boolean => { - for (const perm of permissions) { - const match = userPermMap[formatKey(perm.get())]; + for (const permission of permissions) { + const match = userPermMap[formatKey(permission.get())]; if (match) return true; } return false; From f30a98bbef7e7584d60111c6ea53b0c7932cf3ab Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Fri, 16 Feb 2024 14:03:01 -0800 Subject: [PATCH 09/11] additional refactor --- cmd/ui/src/hooks/usePermissions/usePermissions.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index d609e589b0..4ed83c4b96 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -46,16 +46,14 @@ const usePermissions = () => { const checkAllPermissions = (permissions: Permission[]): boolean => { for (const permission of permissions) { - const match = userPermMap[formatKey(permission.get())]; - if (!match) return false; + if (!checkPermission(permission)) return false; } return true; }; const checkAtLeastOnePermission = (permissions: Permission[]): boolean => { for (const permission of permissions) { - const match = userPermMap[formatKey(permission.get())]; - if (match) return true; + if (checkPermission(permission)) return true; } return false; }; From b41c63b24ad3fe812dbf20872d36846bf4e7f0e5 Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Sat, 24 Feb 2024 20:02:06 -0800 Subject: [PATCH 10/11] refactored permissions definitions --- .../usePermissions/usePermissions.test.tsx | 23 ++- .../hooks/usePermissions/usePermissions.tsx | 25 +++- .../bh-shared-ui/src/utils/permissions.ts | 141 +++++++++++++----- 3 files changed, 141 insertions(+), 48 deletions(-) diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx index 03e42679e0..90c0667fcb 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.test.tsx @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Permission } from 'bh-shared-ui'; +import { Permission, PERMISSIONS } from 'bh-shared-ui'; import { renderHook } from 'src/test-utils'; import usePermissions, { PermissionsFns } from './usePermissions'; @@ -26,7 +26,7 @@ describe('usePermissions', () => { user: { roles: [ { - permissions: permissions.map((p) => p.get()), + permissions: permissions.map((p) => PERMISSIONS[p]), }, ], }, @@ -41,7 +41,7 @@ describe('usePermissions', () => { Permission.APP_READ_APPLICATION_CONFIGURATION, ]; - it('permitted if the user has a required permission', () => { + it('passes check if the user has a required permission', () => { const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); const has = permissions.checkPermission(Permission.CLIENTS_MANAGE); @@ -53,7 +53,7 @@ describe('usePermissions', () => { expect(hasAtLeastOne).toBe(true); }); - it('permitted if the user has multiple required permissions', () => { + it('passes checks if the user has multiple required permissions', () => { const permissions = getPermissionsWithUser(allPermissions); const hasAll = permissions.checkAllPermissions(allPermissions); @@ -63,7 +63,7 @@ describe('usePermissions', () => { expect(hasAtLeastOne).toBe(true); }); - it('denied if the user does not have a matching permission', () => { + it('fails checks if the user does not have a matching permission', () => { const permissions = getPermissionsWithUser([Permission.CLIENTS_MANAGE]); const has = permissions.checkPermission(Permission.AUTH_CREATE_TOKEN); @@ -75,7 +75,7 @@ describe('usePermissions', () => { expect(hasAtLeastOne).toBe(false); }); - it('returns hasAtLeastOne if the user is missing one of many required permissions', () => { + it('passes the check for at least one permission if the user is missing one of many required permissions', () => { const permissions = getPermissionsWithUser([ Permission.APP_READ_APPLICATION_CONFIGURATION, Permission.AUTH_CREATE_TOKEN, @@ -87,4 +87,15 @@ describe('usePermissions', () => { expect(hasAll).toBe(false); expect(hasAtLeastOne).toBe(true); }); + + it('returns a list of the users current permissions', () => { + const permissions = getPermissionsWithUser(allPermissions); + const userPermissionsResult = permissions.getUserPermissions(); + + expect(allPermissions.length).toEqual(userPermissionsResult.length); + + for (const permission of allPermissions) { + expect(userPermissionsResult).toContain(permission); + } + }); }); diff --git a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx index 4ed83c4b96..21570bb2be 100644 --- a/cmd/ui/src/hooks/usePermissions/usePermissions.tsx +++ b/cmd/ui/src/hooks/usePermissions/usePermissions.tsx @@ -14,34 +14,44 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Permission } from 'bh-shared-ui'; +import { Permission, PERMISSIONS } from 'bh-shared-ui'; import { useCallback, useEffect, useState } from 'react'; import { useAppSelector } from 'src/store'; export type PermissionsFns = { + getUserPermissions: () => Permission[]; checkPermission: (permission: Permission) => boolean; checkAllPermissions: (permissions: Permission[]) => boolean; checkAtLeastOnePermission: (permissions: Permission[]) => boolean; }; -const usePermissions = () => { +const usePermissions = (): PermissionsFns => { const auth = useAppSelector((state) => state.auth); const [userPermMap, setUserPermMap] = useState>({}); const formatKey = useCallback((p: { authority: string; name: string }) => `${p.authority}-${p.name}`, []); useEffect(() => { - const userPermissions = auth.user?.roles.map((role) => role.permissions).flat(); - - if (userPermissions) { + if (auth.user) { + const userPermissions = auth.user.roles.map((role) => role.permissions).flat(); const newPermMap: Record = {}; userPermissions.forEach((perm) => (newPermMap[formatKey(perm)] = true)); setUserPermMap(newPermMap); } }, [auth.user, formatKey]); + const getUserPermissions = (): Permission[] => { + if (auth.user) { + return Object.entries(PERMISSIONS) + .filter(([, definition]) => userPermMap[formatKey(definition)]) + .map(([name]) => parseInt(name)); + } + return []; + }; + const checkPermission = (permission: Permission): boolean => { - return !!userPermMap[formatKey(permission.get())]; + const definition = PERMISSIONS[permission]; + return definition && !!userPermMap[formatKey(definition)]; }; const checkAllPermissions = (permissions: Permission[]): boolean => { @@ -58,6 +68,7 @@ const usePermissions = () => { return false; }; - return { checkPermission, checkAllPermissions, checkAtLeastOnePermission }; + return { getUserPermissions, checkPermission, checkAllPermissions, checkAtLeastOnePermission }; }; + export default usePermissions; diff --git a/packages/javascript/bh-shared-ui/src/utils/permissions.ts b/packages/javascript/bh-shared-ui/src/utils/permissions.ts index 1a7af44c1a..19ca6b0ce1 100644 --- a/packages/javascript/bh-shared-ui/src/utils/permissions.ts +++ b/packages/javascript/bh-shared-ui/src/utils/permissions.ts @@ -13,41 +13,112 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 -export class Permission { - private constructor(private readonly authority: string, private readonly name: string) { - this.name = name; - this.authority = authority; - } - - get() { - return { - name: this.name, - authority: this.authority, - }; - } - - static readonly APP_READ_APPLICATION_CONFIGURATION = new Permission('app', 'ReadAppConfig'); - static readonly APP_WRITE_APPLICATION_CONFIGURATION = new Permission('app', 'WriteAppConfig'); - - static readonly APS_GENERATE_REPORT = new Permission('risks', 'GenerateReport'); - static readonly APS_MANAGE_APS = new Permission('risks', 'ManageRisks'); - - static readonly AUTH_ACCEPT_EULA = new Permission('auth', 'AcceptEULA'); - static readonly AUTH_CREATE_TOKEN = new Permission('auth', 'CreateToken'); - static readonly AUTH_MANAGE_APPLICATION_CONFIGURATIONS = new Permission('auth', 'ManageAppConfig'); - static readonly AUTH_MANAGE_PROVIDERS = new Permission('auth', 'ManageProviders'); - static readonly AUTH_MANAGE_SELF = new Permission('auth', 'ManageSelf'); - static readonly AUTH_MANAGE_USERS = new Permission('auth', 'ManageUsers'); - - static readonly CLIENTS_MANAGE = new Permission('clients', 'Manage'); - static readonly CLIENTS_READ = new Permission('clients', 'Read'); - static readonly CLIENTS_TASKING = new Permission('clients', 'Tasking'); +export enum Permission { + APP_READ_APPLICATION_CONFIGURATION, + APP_WRITE_APPLICATION_CONFIGURATION, + APS_GENERATE_REPORT, + APS_MANAGE_APS, + AUTH_ACCEPT_EULA, + AUTH_CREATE_TOKEN, + AUTH_MANAGE_APPLICATION_CONFIGURATIONS, + AUTH_MANAGE_PROVIDERS, + AUTH_MANAGE_SELF, + AUTH_MANAGE_USERS, + CLIENTS_MANAGE, + CLIENTS_READ, + CLIENTS_TASKING, + COLLECTION_MANAGE_JOBS, + GRAPH_DB_READ, + GRAPH_DB_WRITE, + SAVED_QUERIES_READ, + SAVED_QUERIES_WRITE, + FAKE_PERMISSION, +} - static readonly COLLECTION_MANAGE_JOBS = new Permission('collection', 'ManageJobs'); +export type PermissionDefinition = { + authority: string; + name: string; +}; - static readonly GRAPH_DB_READ = new Permission('graphdb', 'Read'); - static readonly GRAPH_DB_WRITE = new Permission('graphdb', 'Write'); +export type PermissionDefinitions = { + [index: number]: PermissionDefinition; +}; - static readonly SAVED_QUERIES_READ = new Permission('saved_queries', 'Read'); - static readonly SAVED_QUERIES_WRITE = new Permission('saved_queries', 'Write'); -} +export const PERMISSIONS: PermissionDefinitions = { + [Permission.APP_READ_APPLICATION_CONFIGURATION]: { + authority: 'app', + name: 'ReadAppConfig', + }, + [Permission.APP_WRITE_APPLICATION_CONFIGURATION]: { + authority: 'app', + name: 'WriteAppConfig', + }, + [Permission.APS_GENERATE_REPORT]: { + authority: 'risks', + name: 'GenerateReport', + }, + [Permission.APS_MANAGE_APS]: { + authority: 'risks', + name: 'ManageRisks', + }, + [Permission.AUTH_ACCEPT_EULA]: { + authority: 'auth', + name: 'AcceptEULA', + }, + [Permission.AUTH_CREATE_TOKEN]: { + authority: 'auth', + name: 'CreateToken', + }, + [Permission.AUTH_MANAGE_APPLICATION_CONFIGURATIONS]: { + authority: 'auth', + name: 'ManageAppConfig', + }, + [Permission.AUTH_MANAGE_PROVIDERS]: { + authority: 'auth', + name: 'ManageProviders', + }, + [Permission.AUTH_MANAGE_SELF]: { + authority: 'auth', + name: 'ManageSelf', + }, + [Permission.AUTH_MANAGE_USERS]: { + authority: 'auth', + name: 'ManageUsers', + }, + [Permission.CLIENTS_MANAGE]: { + authority: 'clients', + name: 'Manage', + }, + [Permission.CLIENTS_READ]: { + authority: 'clients', + name: 'Read', + }, + [Permission.CLIENTS_TASKING]: { + authority: 'clients', + name: 'Tasking', + }, + [Permission.COLLECTION_MANAGE_JOBS]: { + authority: 'collection', + name: 'ManageJobs', + }, + [Permission.GRAPH_DB_READ]: { + authority: 'graphdb', + name: 'Read', + }, + [Permission.GRAPH_DB_WRITE]: { + authority: 'graphdb', + name: 'Write', + }, + [Permission.SAVED_QUERIES_READ]: { + authority: 'saved_queries', + name: 'Read', + }, + [Permission.SAVED_QUERIES_WRITE]: { + authority: 'saved_queries', + name: 'Write', + }, + [Permission.FAKE_PERMISSION]: { + authority: 'fake', + name: 'permission', + }, +}; From d649ab4e5c43cd50ef851b08d4875f583a4e58ed Mon Sep 17 00:00:00 2001 From: Wesley Robert Maffly-Kipp Date: Wed, 28 Feb 2024 14:48:21 -0800 Subject: [PATCH 11/11] removed fake permission --- packages/javascript/bh-shared-ui/src/utils/permissions.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/javascript/bh-shared-ui/src/utils/permissions.ts b/packages/javascript/bh-shared-ui/src/utils/permissions.ts index 19ca6b0ce1..8894bc0f8d 100644 --- a/packages/javascript/bh-shared-ui/src/utils/permissions.ts +++ b/packages/javascript/bh-shared-ui/src/utils/permissions.ts @@ -32,7 +32,6 @@ export enum Permission { GRAPH_DB_WRITE, SAVED_QUERIES_READ, SAVED_QUERIES_WRITE, - FAKE_PERMISSION, } export type PermissionDefinition = { @@ -117,8 +116,4 @@ export const PERMISSIONS: PermissionDefinitions = { authority: 'saved_queries', name: 'Write', }, - [Permission.FAKE_PERMISSION]: { - authority: 'fake', - name: 'permission', - }, };