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 @@
-
\ No newline at end of file
+
+
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 @@
-
\ No newline at end of file
+
+
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 @@
-
\ No newline at end of file
+
+
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;
+};