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