diff --git a/src/components/Misc/RestrictedView.tsx b/src/components/Misc/RestrictedView.tsx new file mode 100644 index 00000000..e2214d19 --- /dev/null +++ b/src/components/Misc/RestrictedView.tsx @@ -0,0 +1,20 @@ +import { Stack, Text } from '@mantine/core'; + +const RestrictedView = () => { + return ( + + + Access restricted, Please contact your administrator. + + + ); +}; + +export default RestrictedView; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 2c63ca9d..64fbe52c 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -27,7 +27,8 @@ import { signOutHandler } from '@/utils'; import { appStoreReducers, useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import _ from 'lodash'; -const { setUserRoles, setUserSpecificStreams, setUserAccessMap, changeStream } = appStoreReducers; +const { setUserRoles, setUserSpecificStreams, setUserAccessMap, changeStream, setStreamSpecificUserAccess } = + appStoreReducers; const navItems = [ { @@ -132,8 +133,10 @@ const Navbar: FC = () => { setAppStore((store) => setUserSpecificStreams(store, null)); } } - setAppStore((store) => setUserAccessMap(store, getStreamsSepcificAccess(getUserRolesData?.data))); - }, [getUserRolesData?.data, getLogStreamListData?.data]); + const streamSpecificAccess = getStreamsSepcificAccess(getUserRolesData?.data, streamName); + setAppStore((store) => setStreamSpecificUserAccess(store, streamSpecificAccess)); + setAppStore((store) => setUserAccessMap(store, streamSpecificAccess)); + }, [getUserRolesData?.data, getLogStreamListData?.data, streamName]); useEffect(() => { getUserRolesMutation({ userName: username ? username : '' }); @@ -174,7 +177,7 @@ const Navbar: FC = () => { {previlagedActions.map((navItem, index) => { if (isStandAloneMode === null) return null; if (navItem.route === USERS_MANAGEMENT_ROUTE && !userAccessMap.hasUserAccess) return null; - if (navItem.route === CLUSTER_ROUTE && (!userAccessMap.hasUserAccess || isStandAloneMode)) return null; + if (navItem.route === CLUSTER_ROUTE && (!userAccessMap.hasClusterAccess || isStandAloneMode)) return null; const isActiveItem = navItem.route === currentRoute; return ( diff --git a/src/components/Navbar/rolesHandler.ts b/src/components/Navbar/rolesHandler.ts index 5a9c3f37..79b16838 100644 --- a/src/components/Navbar/rolesHandler.ts +++ b/src/components/Navbar/rolesHandler.ts @@ -4,6 +4,7 @@ const adminAccess = [ 'Ingest', 'Query', 'CreateStream', + 'DeleteStream', 'ListStream', 'GetSchema', 'GetStats', @@ -19,11 +20,16 @@ const adminAccess = [ 'PutRoles', 'GetRole', 'Cluster', + 'Dashboard', + 'Alerts', + 'Users', + 'StreamSettings', // retention & hot-tier ]; const editorAccess = [ 'Ingest', 'Query', 'CreateStream', + 'DeleteStream', 'ListStream', 'GetSchema', 'GetStats', @@ -31,6 +37,9 @@ const editorAccess = [ 'PutRetention', 'PutAlert', 'GetAlert', + 'Dashboard', + 'Alerts', + 'StreamSettings', // retention & hot-tier ]; const writerAccess = [ 'Ingest', @@ -42,11 +51,27 @@ const writerAccess = [ 'PutAlert', 'GetAlert', 'GetLiveTail', + 'Dashboard', + 'Alerts', + 'StreamSettings', // retention & hot-tier +]; +const readerAccess = [ + 'Query', + 'ListStream', + 'GetSchema', + 'GetStats', + 'GetRetention', + 'GetAlert', + 'GetLiveTail', + 'Dashboard', ]; -const readerAccess = ['Query', 'ListStream', 'GetSchema', 'GetStats', 'GetRetention', 'GetAlert', 'GetLiveTail']; const ingestorAccess = ['Ingest']; -const getStreamsSepcificAccess = (rolesWithRoleName: UserRoles, stream?: string) => { +const getStreamsSepcificAccess = (rolesWithRoleName: UserRoles | null, stream?: string): string[] | null => { + if (!rolesWithRoleName) { + return null; + } + let access: string[] = []; let roles: any[] = []; for (var prop in rolesWithRoleName) { diff --git a/src/hooks/useAlertsEditor.tsx b/src/hooks/useAlertsEditor.tsx index 05c7b451..ab8f00fb 100644 --- a/src/hooks/useAlertsEditor.tsx +++ b/src/hooks/useAlertsEditor.tsx @@ -5,14 +5,14 @@ import { AxiosError, isAxiosError } from 'axios'; import { useStreamStore, streamStoreReducers } from '@/pages/Stream/providers/StreamProvider'; const { setAlertsConfig } = streamStoreReducers; -const useAlertsQuery = (streamName: string) => { +const useAlertsQuery = (streamName: string, hasAlertsAccess: boolean) => { const [, setStreamStore] = useStreamStore((_store) => null); const { data, isError, isSuccess, isLoading, refetch } = useQuery( - ['fetch-log-stream-alert', streamName], + ['fetch-log-stream-alert', streamName, hasAlertsAccess], () => getLogStreamAlerts(streamName), { retry: false, - enabled: streamName !== '', + enabled: streamName !== '' && hasAlertsAccess, refetchOnWindowFocus: false, onSuccess: (data) => { setStreamStore((store) => setAlertsConfig(store, data)); diff --git a/src/hooks/useGetStreamMetadata.ts b/src/hooks/useGetStreamMetadata.ts index b3537b6f..3860cb09 100644 --- a/src/hooks/useGetStreamMetadata.ts +++ b/src/hooks/useGetStreamMetadata.ts @@ -1,5 +1,8 @@ import { LogStreamRetention, LogStreamStat } from '@/@types/parseable/api/stream'; import { getLogStreamRetention, getLogStreamStats } from '@/api/logStream'; +import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import _ from 'lodash'; import { useCallback, useState } from 'react'; type MetaData = { @@ -14,6 +17,7 @@ export const useGetStreamMetadata = () => { const [isLoading, setLoading] = useState(false); const [error, setError] = useState(false); const [metaData, setMetadata] = useState(null); + const [userRoles] = useAppStore((store) => store.userRoles); const getStreamMetadata = useCallback(async (streams: string[]) => { setLoading(true); @@ -23,7 +27,10 @@ export const useGetStreamMetadata = () => { const allStatsRes = await Promise.all(allStatsReqs); // retention - const allretentionReqs = streams.map((stream) => getLogStreamRetention(stream)); + const streamsWithSettingsAccess = _.filter(streams, (stream) => + _.includes(getStreamsSepcificAccess(userRoles, stream), 'StreamSettings'), + ); + const allretentionReqs = streamsWithSettingsAccess.map((stream) => getLogStreamRetention(stream)); const allretentionRes = await Promise.all(allretentionReqs); const metadata = streams.reduce((acc, stream, index) => { diff --git a/src/hooks/useHotTier.ts b/src/hooks/useHotTier.ts index 6c2e82c8..ecd928e1 100644 --- a/src/hooks/useHotTier.ts +++ b/src/hooks/useHotTier.ts @@ -6,7 +6,7 @@ import { AxiosError, isAxiosError } from 'axios'; const { setHotTier } = streamStoreReducers; -export const useHotTier = (streamName: string) => { +export const useHotTier = (streamName: string, hasSettingsAccess: boolean) => { const [, setStreamStore] = useStreamStore((_store) => null); const { refetch: refetchHotTierInfo, @@ -14,7 +14,7 @@ export const useHotTier = (streamName: string) => { isLoading: getHotTierInfoLoading, } = useQuery(['fetch-hot-tier-info', streamName], () => getHotTierInfo(streamName), { retry: false, - enabled: streamName !== '', + enabled: streamName !== '' && hasSettingsAccess, refetchOnWindowFocus: false, onSuccess: (data) => { setStreamStore((store) => setHotTier(store, data.data)); diff --git a/src/hooks/useLoginForm.ts b/src/hooks/useLoginForm.ts index e41e1022..a7c38b8b 100644 --- a/src/hooks/useLoginForm.ts +++ b/src/hooks/useLoginForm.ts @@ -10,6 +10,8 @@ import { useId } from '@mantine/hooks'; import { useEffect } from 'react'; import Cookies from 'js-cookie'; import { getQueryParam } from '@/utils'; +import { isAxiosError } from 'axios'; +import _ from 'lodash'; export const useLoginForm = () => { const notificationId = useId(); @@ -78,7 +80,22 @@ export const useLoginForm = () => { } } } catch (err) { - notifyError({ message: 'Something went wrong' }); + if (isAxiosError(err)) { + const errStatus = err.response?.status; + if (errStatus === 401) { + setError('Unauthorized User'); + notifyError({ message: 'The request failed with a status code of 401' }); + } else { + const errMsg = _.isString(err.response?.data) + ? err.response?.data || 'Something went wrong' + : 'Something went wrong'; + setError(errMsg); + notifyError({ message: errMsg }); + } + } else { + setError('Request Failed!'); + notifyError({ message: 'Something went wrong' }); + } } finally { setLoading(false); } diff --git a/src/hooks/useRetentionEditor.tsx b/src/hooks/useRetentionEditor.tsx index ffbd5f1e..18ee3451 100644 --- a/src/hooks/useRetentionEditor.tsx +++ b/src/hooks/useRetentionEditor.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; const { setRetention } = streamStoreReducers; -export const useRetentionQuery = (streamName: string) => { +export const useRetentionQuery = (streamName: string, hasSettingsAccess: boolean) => { const [, setStreamStore] = useStreamStore((_store) => null); const { data: getLogRetentionData, @@ -29,7 +29,7 @@ export const useRetentionQuery = (streamName: string) => { } }, retry: false, - enabled: streamName !== '', + enabled: streamName !== '' && hasSettingsAccess, refetchOnWindowFocus: false, }); diff --git a/src/layouts/MainLayout/providers/AppProvider.tsx b/src/layouts/MainLayout/providers/AppProvider.tsx index d4a8b225..fd2f14b5 100644 --- a/src/layouts/MainLayout/providers/AppProvider.tsx +++ b/src/layouts/MainLayout/providers/AppProvider.tsx @@ -1,5 +1,4 @@ import { LogStreamData } from '@/@types/parseable/api/stream'; -import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; import initContext from '@/utils/initContext'; import { AboutData } from '@/@types/parseable/api/about'; import _ from 'lodash'; @@ -40,7 +39,7 @@ type AppStoreReducers = { setUserRoles: (store: AppStore, roles: UserRoles | null) => ReducerOutput; setUserSpecificStreams: (store: AppStore, userSpecficStreams: LogStreamData | null) => ReducerOutput; setUserAccessMap: (store: AppStore, accessRoles: string[] | null) => ReducerOutput; - setStreamSpecificUserAccess: (store: AppStore) => ReducerOutput; + setStreamSpecificUserAccess: (store: AppStore, streamSpecificUserAccess: string[] | null) => ReducerOutput; setInstanceConfig: (store: AppStore, instanceConfig: AboutData) => ReducerOutput; toggleCreateStreamModal: (store: AppStore, val?: boolean) => ReducerOutput; setSavedFilters: (store: AppStore, savedFilters: AxiosResponse) => ReducerOutput; @@ -65,11 +64,13 @@ const { Provider: AppProvider, useStore: useAppStore } = initContext(initialStat // helpers const accessKeyMap: { [key: string]: string } = { - hasUserAccess: 'ListUser', + hasUserAccess: 'Users', hasDeleteAccess: 'DeleteStream', - hasUpdateAlertAccess: 'PutAlert', - hasGetAlertAccess: 'GetAlert', hasCreateStreamAccess: 'CreateStream', + hasDeleteStreamAccess: 'DeleteStream', + hasClusterAccess: 'Cluster', + hasAlertsAccess: 'Alerts', + hasSettingsAccess: 'StreamSettings', }; const generateUserAcccessMap = (accessRoles: string[] | null) => { @@ -113,12 +114,8 @@ const setUserAccessMap = (_store: AppStore, accessRoles: string[] | null) => { return { userAccessMap: generateUserAcccessMap(accessRoles) }; }; -const setStreamSpecificUserAccess = (store: AppStore) => { - if (store.userRoles && store.currentStream) { - return { streamSpecificUserAccess: getStreamsSepcificAccess(store.userRoles, store.currentStream) }; - } else { - return store; - } +const setStreamSpecificUserAccess = (_store: AppStore, streamSpecificUserAccess: string[] | null) => { + return { streamSpecificUserAccess }; }; const setInstanceConfig = (_store: AppStore, instanceConfig: AboutData | null) => { diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index d877c700..caf85708 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -11,6 +11,8 @@ import cardStyles from './styles/Card.module.css'; import homeStyles from './styles/Home.module.css'; import CreateStreamModal from './CreateStreamModal'; import { useAppStore, appStoreReducers } from '@/layouts/MainLayout/providers/AppProvider'; +import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; +import _ from 'lodash'; const { changeStream, toggleCreateStreamModal } = appStoreReducers; @@ -40,6 +42,7 @@ const Home: FC = () => { const navigate = useNavigate(); const { getStreamMetadata, metaData } = useGetStreamMetadata(); const [userSpecificStreams, setAppStore] = useAppStore((store) => store.userSpecificStreams); + const [userRoles] = useAppStore((store) => store.userRoles); const [userAccessMap] = useAppStore((store) => store.userAccessMap); useEffect(() => { @@ -89,7 +92,16 @@ const Home: FC = () => { ) : ( {Object.entries(metaData || {}).map(([stream, data]) => { - return ; + const hasSettingsAccess = _.includes(getStreamsSepcificAccess(userRoles, stream), 'StreamSettings'); + return ( + + ); })} )} @@ -131,6 +143,7 @@ type StreamInfoProps = { retention: LogStreamRetention | []; }; navigateToStream: (stream: string) => void; + hasSettingsAccess: boolean; }; const StreamInfo: FC = (props) => { @@ -163,7 +176,7 @@ const StreamInfo: FC = (props) => { - + {props.hasSettingsAccess && } diff --git a/src/pages/Stream/Views/Manage/Alerts.tsx b/src/pages/Stream/Views/Manage/Alerts.tsx index 2a83b14f..1a2abcf6 100644 --- a/src/pages/Stream/Views/Manage/Alerts.tsx +++ b/src/pages/Stream/Views/Manage/Alerts.tsx @@ -19,6 +19,7 @@ import { IconEdit, IconInfoCircleFilled, IconPlus, IconTrash } from '@tabler/ico import { UseFormReturnType, useForm } from '@mantine/form'; import { useStreamStore, streamStoreReducers } from '../../providers/StreamProvider'; import ErrorView from './ErrorView'; +import RestrictedView from '@/components/Misc/RestrictedView'; const defaultColumnTypeConfig = { column: '', operator: '=', value: '', repeats: 1, ignoreCase: false }; const defaultColumnTypeRule = { type: 'column' as 'column', config: defaultColumnTypeConfig }; @@ -573,16 +574,15 @@ const AlertsModal = (props: { ); }; -const Header = (props: { selectAlert: selectAlert; isLoading: boolean }) => { +const Header = (props: { selectAlert: selectAlert; isLoading: boolean, showCreateBtn: boolean }) => { return ( Alerts - {!props.isLoading && ( + {!props.isLoading && props.showCreateBtn && ( @@ -659,6 +659,7 @@ const Alerts = (props: { isLoading: boolean; schemaLoading: boolean; isError: boolean; + hasAlertsAccess: boolean; updateAlerts: ({ config, onSuccess }: { config: any; onSuccess?: () => void }) => void; }) => { const [alertName, setAlertName] = useState(''); @@ -676,9 +677,11 @@ const Alerts = (props: { return ( -
+
{props.isError ? ( + ) : !props.hasAlertsAccess ? ( + ) : ( { const [currentStream] = useAppStore((store) => store.currentStream); const [instanceConfig] = useAppStore((store) => store.instanceConfig); - const getStreamAlertsConfig = useAlertsQuery(currentStream || ''); + const [userAccessMap] = useAppStore((store) => store.userAccessMap); + const { hasAlertsAccess, hasSettingsAccess } = userAccessMap; + const getStreamAlertsConfig = useAlertsQuery(currentStream || '', hasAlertsAccess); const getStreamStats = useLogStreamStats(currentStream || ''); - const getRetentionConfig = useRetentionQuery(currentStream || ''); + const getRetentionConfig = useRetentionQuery(currentStream || '', hasSettingsAccess); const getStreamInfo = useGetStreamInfo(currentStream || '', currentStream !== null); - const hotTierFetch = useHotTier(currentStream || ''); + const hotTierFetch = useHotTier(currentStream || '', hasSettingsAccess); // todo - handle loading and error states separately const isAlertsLoading = getStreamAlertsConfig.isError || getStreamAlertsConfig.isLoading; @@ -45,6 +47,7 @@ const Management = (props: { schemaLoading: boolean }) => { isDeleting={hotTierFetch.isDeleting} isUpdating={hotTierFetch.isUpdating} isRetentionError={getRetentionConfig.getLogRetentionIsError} + hasSettingsAccess={hasSettingsAccess} /> { schemaLoading={props.schemaLoading} updateAlerts={getStreamAlertsConfig.updateLogStreamAlerts} isError={getStreamAlertsConfig.isError} + hasAlertsAccess={hasAlertsAccess} /> diff --git a/src/pages/Stream/Views/Manage/Settings.tsx b/src/pages/Stream/Views/Manage/Settings.tsx index 081904e4..bf32e28e 100644 --- a/src/pages/Stream/Views/Manage/Settings.tsx +++ b/src/pages/Stream/Views/Manage/Settings.tsx @@ -10,6 +10,7 @@ import { IconCheck, IconTrash, IconX } from '@tabler/icons-react'; import { sanitizeBytes, convertGibToBytes } from '@/utils/formatBytes'; import timeRangeUtils from '@/utils/timeRangeUtils'; import ErrorView from './ErrorView'; +import RestrictedView from '@/components/Misc/RestrictedView'; const { formatDateWithTimezone } = timeRangeUtils; @@ -313,6 +314,7 @@ const Settings = (props: { isDeleting: boolean; isUpdating: boolean; isRetentionError: boolean; + hasSettingsAccess: boolean; }) => { const [isStandAloneMode] = useAppStore((store) => store.isStandAloneMode); return ( @@ -327,23 +329,29 @@ const Settings = (props: { ) : ( <> - {!isStandAloneMode && ( - + {!props.hasSettingsAccess ? ( + + ) : ( + <> + {!isStandAloneMode && ( + + )} + + + Retention + {!props.isRetentionError ? ( + + ) : ( + + )} + + )} - - - Retention - {!props.isRetentionError ? ( - - ) : ( - - )} - )} diff --git a/src/pages/Stream/components/PrimaryToolbar.tsx b/src/pages/Stream/components/PrimaryToolbar.tsx index e1dfd010..8ccd45ac 100644 --- a/src/pages/Stream/components/PrimaryToolbar.tsx +++ b/src/pages/Stream/components/PrimaryToolbar.tsx @@ -89,6 +89,7 @@ const ViewToggle = () => { const PrimaryToolbar = () => { const [maximized] = useAppStore((store) => store.maximized); + const [hasDeleteStreamAccess] = useAppStore(store => store.userAccessMap.hasDeleteStreamAccess) const { view } = useParams(); useEffect(() => { @@ -129,7 +130,7 @@ const PrimaryToolbar = () => { ) : view === 'manage' ? ( - + {hasDeleteStreamAccess && } ) : null} diff --git a/src/routes/AccessSpecificRoute.tsx b/src/routes/AccessSpecificRoute.tsx index 5f1f53ad..7fc73dbb 100644 --- a/src/routes/AccessSpecificRoute.tsx +++ b/src/routes/AccessSpecificRoute.tsx @@ -1,8 +1,8 @@ -import { LOGIN_ROUTE } from '@/constants/routes'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import { useEffect, type FC } from 'react'; -import { Outlet, useNavigate } from 'react-router-dom'; - +import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import _ from 'lodash'; +import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler'; interface AccessSpecificRouteProps { accessRequired: string[]; } @@ -10,17 +10,22 @@ interface AccessSpecificRouteProps { const AccessSpecificRoute: FC = (props) => { const { accessRequired } = props; const navigate = useNavigate(); + const { streamName } = useParams(); - const [streamSpecificUserAccess] = useAppStore((store) => store.streamSpecificUserAccess); - + const [userRoles] = useAppStore((store) => store.userRoles); + const streamSpecificAccess = getStreamsSepcificAccess(userRoles, streamName); useEffect(() => { if ( - streamSpecificUserAccess && - !streamSpecificUserAccess?.some((access: string) => accessRequired.includes(access)) + streamSpecificAccess !== null && + !streamSpecificAccess?.some((access: string) => accessRequired.includes(access)) ) { - navigate(LOGIN_ROUTE); + navigate('/'); } - }, [streamSpecificUserAccess]); + }, [streamSpecificAccess]); + + if (streamSpecificAccess === null) { + return null; + } return ; }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 69b89259..b3f30810 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -33,7 +33,7 @@ const AppRouter: FC = () => { }> } /> } /> - }> + }> } /> }> diff --git a/src/utils/index.ts b/src/utils/index.ts index e8826d2a..5862a270 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -64,12 +64,18 @@ export const generateQueryParam = (obj: object) => { }; export const signOutHandler = async () => { + const loginPage = `${window.location.origin}/login`; + const currentPage = window.location.href; try { await logOut(); Cookies.remove('session'); Cookies.remove('username'); - window.location.href = `${window.location.origin}/login`; + if (currentPage !== loginPage) { + window.location.href = loginPage; + } } catch (e) { + Cookies.remove('session'); + Cookies.remove('username'); console.log(e); } };