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; +};