Domain restriction for non domain entities (#17314)

* add forbidden handling of entities

* add forbidden page

* move forbidden page to authenticated app router

* fix domain tests
This commit is contained in:
Karan Hotchandani 2024-08-07 16:50:17 +05:30 committed by GitHub
parent 4c43509203
commit c6e7fe9423
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 148 additions and 12 deletions

View File

@ -12,9 +12,11 @@
*/
import test, { expect } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { get } from 'lodash';
import { SidebarItem } from '../../constant/sidebar';
import { DataProduct } from '../../support/domain/DataProduct';
import { Domain } from '../../support/domain/Domain';
import { ENTITY_PATH } from '../../support/entity/Entity.interface';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import { getApiContext, redirectToHomePage } from '../../utils/common';
@ -31,7 +33,7 @@ import {
verifyDomain,
} from '../../utils/domain';
import { sidebarClick } from '../../utils/sidebar';
import { performUserLogin } from '../../utils/user';
import { performUserLogin, visitUserProfilePage } from '../../utils/user';
test.describe('Domains', () => {
test.use({ storageState: 'playwright/.auth/admin.json' });
@ -181,6 +183,7 @@ test.describe('Domains', () => {
await checkAssetsCount(page, assets.length);
await domain.delete(apiContext);
await assetCleanup();
await afterAction();
});
});
@ -194,7 +197,7 @@ test.describe('Domains Rbac', () => {
const user1 = new UserClass();
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
const { apiContext, afterAction, page } = await performAdminLogin(browser);
await domain1.create(apiContext);
await domain2.create(apiContext);
await domain3.create(apiContext);
@ -220,6 +223,24 @@ test.describe('Domains Rbac', () => {
];
await user1.patch({ apiContext, patchData: domainPayload });
// Add domain role to the user
await visitUserProfilePage(page, user1.responseData.name);
await page
.getByTestId('user-profile')
.locator('.ant-collapse-expand-icon')
.click();
await page.getByTestId('edit-roles-button').click();
await page
.getByTestId('select-user-roles')
.getByLabel('Select roles')
.click();
await page.getByText('Domain Only Access Role').click();
await page.click('body');
const patchRes = page.waitForResponse('/api/v1/users/*');
await page.getByTestId('inline-save-btn').click();
await patchRes;
await afterAction();
});
@ -265,6 +286,24 @@ test.describe('Domains Rbac', () => {
.locator('span')
).toBeVisible();
for (const asset of domainAssset2) {
const fqn = encodeURIComponent(
get(asset, 'entityResponseData.fullyQualifiedName', '')
);
const assetData = userPage.waitForResponse(
`/api/v1/${asset.endpoint}/name/${fqn}*`
);
await userPage.goto(`/${ENTITY_PATH[asset.endpoint]}/${fqn}`);
await assetData;
await expect(
userPage.getByTestId('permission-error-placeholder')
).toHaveText(
'You dont have access, please check with the admin to get permissions'
);
}
await afterActionUser1();
});

View File

@ -12,8 +12,10 @@
*/
import { expect, Page } from '@playwright/test';
import { GlobalSettingOptions } from '../constant/settings';
import { UserClass } from '../support/user/UserClass';
import { getAuthContext, getToken, toastNotification } from './common';
import { settingClick } from './sidebar';
export const performUserLogin = async (browser, user: UserClass) => {
const page = await browser.newPage();
@ -76,6 +78,16 @@ export const deletedUserChecks = async (page: Page) => {
).not.toBeVisible();
};
export const visitUserProfilePage = async (page: Page, userName: string) => {
await settingClick(page, GlobalSettingOptions.USERS);
const userResponse = page.waitForResponse(
'/api/v1/search/query?q=**&from=0&size=*&index=*'
);
await page.getByTestId('searchbar').fill(userName);
await userResponse;
await page.getByTestId(userName).click();
};
export const softDeleteUserProfilePage = async (
page: Page,
userName: string,

View File

@ -23,6 +23,7 @@ import { Operation } from '../../generated/entity/policies/policy';
import AddCustomMetricPage from '../../pages/AddCustomMetricPage/AddCustomMetricPage';
import { CustomizablePage } from '../../pages/CustomizablePage/CustomizablePage';
import DataQualityPage from '../../pages/DataQuality/DataQualityPage';
import ForbiddenPage from '../../pages/ForbiddenPage/ForbiddenPage';
import { checkPermission, userPermissions } from '../../utils/PermissionsUtils';
import AdminProtectedRoute from './AdminProtectedRoute';
import withSuspenseFallback from './withSuspenseFallback';
@ -265,6 +266,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
return (
<Switch>
<Route exact component={ForbiddenPage} path={ROUTES.FORBIDDEN} />
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
<Route exact component={TourPageComponent} path={ROUTES.TOUR} />
<Route exact component={ExplorePageV1} path={ROUTES.EXPLORE} />

View File

@ -30,7 +30,7 @@ const SuggestionsSlider = () => {
} = useSuggestionsContext();
return (
<div className="d-flex items-center gap-2">
<div className="d-flex items-center gap-2 m-r-md">
<Typography.Text className="right-panel-label">
{t('label.suggested-description-plural')}
</Typography.Text>

View File

@ -135,6 +135,7 @@ export const ROUTES = {
SAML_CALLBACK: '/saml/callback',
SILENT_CALLBACK: '/silent-callback',
NOT_FOUND: '/404',
FORBIDDEN: '/403',
MY_DATA: '/my-data',
TOUR: '/tour',
REPORTS: '/reports',

View File

@ -42,7 +42,7 @@ export const useDomainStore = create<DomainStore>()(
userDomainsObj.map((item) => item.fullyQualifiedName) ?? [];
let filteredDomains: Domain[] = data;
if (domains.length > 0) {
if (domains.length > 0 && !isAdmin) {
filteredDomains = data.filter((domain) =>
userDomainFqn.includes(domain.fullyQualifiedName)
);

View File

@ -46,6 +46,7 @@ import {
getEntityDetailsPath,
getVersionPath,
INITIAL_PAGING_VALUE,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
@ -53,6 +54,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import {
EntityTabs,
@ -213,6 +215,9 @@ const APICollectionPage: FunctionComponent = () => {
setShowDeletedEndpoints(response.deleted ?? false);
} catch (err) {
// Error
if ((err as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsAPICollectionLoading(false);
}

View File

@ -22,12 +22,13 @@ import APIEndpointDetails from '../../components/APIEndpoint/APIEndpointDetails/
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../components/common/Loader/Loader';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import { getVersionPath } from '../../constants/constants';
import { getVersionPath, ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
@ -154,6 +155,10 @@ const APIEndpointPage = () => {
} catch (error) {
if ((error as AxiosError).response?.status === 404) {
setIsError(true);
} else if (
(error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN
) {
history.replace(ROUTES.FORBIDDEN);
} else {
showErrorToast(
error as AxiosError,

View File

@ -41,6 +41,7 @@ import { SourceType } from '../../components/SearchedData/SearchedData.interface
import {
getEntityDetailsPath,
getVersionPath,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import LineageProvider from '../../context/LineageProvider/LineageProvider';
@ -49,6 +50,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import {
EntityTabs,
@ -150,6 +152,9 @@ const ContainerPage = () => {
} catch (error) {
showErrorToast(error as AxiosError);
setHasError(true);
if ((error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsLoading(false);
}

View File

@ -22,9 +22,10 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error
import Loader from '../../components/common/Loader/Loader';
import DashboardDetails from '../../components/Dashboard/DashboardDetails/DashboardDetails.component';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import { getVersionPath } from '../../constants/constants';
import { getVersionPath, ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
@ -151,6 +152,10 @@ const DashboardDetailsPage = () => {
} catch (error) {
if ((error as AxiosError).response?.status === 404) {
setIsError(true);
} else if (
(error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN
) {
history.replace(ROUTES.FORBIDDEN);
} else {
showErrorToast(
error as AxiosError,

View File

@ -24,15 +24,18 @@ import {
} from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../components/common/Loader/Loader';
import DataModelDetails from '../../components/Dashboard/DataModel/DataModels/DataModelDetails.component';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import { ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { TabSpecificField } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
@ -61,6 +64,7 @@ import { showErrorToast } from '../../utils/ToastUtils';
const DataModelsPage = () => {
const { t } = useTranslation();
const history = useHistory();
const { currentUser } = useApplicationStore();
const { getEntityPermissionByFqn } = usePermissionProvider();
@ -133,6 +137,9 @@ const DataModelsPage = () => {
} catch (error) {
showErrorToast(error as AxiosError);
setHasError(true);
if ((error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsLoading(false);
}

View File

@ -48,6 +48,7 @@ import {
getEntityDetailsPath,
getExplorePath,
getVersionPath,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
@ -55,6 +56,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import {
EntityTabs,
@ -214,8 +216,13 @@ const DatabaseDetails: FunctionComponent = () => {
setServiceType(serviceType);
}
})
.catch(() => {
.catch((error) => {
// Error
if (
(error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN
) {
history.replace(ROUTES.FORBIDDEN);
}
})
.finally(() => {
setIsLoading(false);

View File

@ -47,6 +47,7 @@ import {
getEntityDetailsPath,
getVersionPath,
INITIAL_PAGING_VALUE,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
@ -54,6 +55,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import {
EntityTabs,
@ -229,6 +231,9 @@ const DatabaseSchemaPage: FunctionComponent = () => {
setShowDeletedTables(response.deleted ?? false);
} catch (err) {
// Error
if ((err as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsSchemaDetailsLoading(false);
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* 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.
*/
import React from 'react';
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
const ForbiddenPage = () => {
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
};
export default ForbiddenPage;

View File

@ -22,9 +22,10 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error
import Loader from '../../components/common/Loader/Loader';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import MlModelDetailComponent from '../../components/MlModel/MlModelDetail/MlModelDetail.component';
import { getVersionPath } from '../../constants/constants';
import { getVersionPath, ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
@ -107,6 +108,9 @@ const MlModelPage = () => {
});
} catch (error) {
showErrorToast(error as AxiosError);
if ((error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsDetailLoading(false);
}

View File

@ -21,9 +21,10 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error
import Loader from '../../components/common/Loader/Loader';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import PipelineDetails from '../../components/Pipeline/PipelineDetails/PipelineDetails.component';
import { getVersionPath } from '../../constants/constants';
import { getVersionPath, ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType } from '../../enums/entity.enum';
import { Pipeline } from '../../generated/entity/data/pipeline';
@ -135,6 +136,10 @@ const PipelineDetailsPage = () => {
} catch (error) {
if ((error as AxiosError).response?.status === 404) {
setIsError(true);
} else if (
(error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN
) {
history.replace(ROUTES.FORBIDDEN);
} else {
showErrorToast(
error as AxiosError,

View File

@ -40,6 +40,7 @@ import { SourceType } from '../../components/SearchedData/SearchedData.interface
import {
getEntityDetailsPath,
getVersionPath,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import LineageProvider from '../../context/LineageProvider/LineageProvider';
@ -48,6 +49,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { CSMode } from '../../enums/codemirror.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityTabs, EntityType } from '../../enums/entity.enum';
@ -198,7 +200,9 @@ const StoredProcedurePage = () => {
id: response.id ?? '',
});
} catch (error) {
// Error here
if ((error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setIsLoading(false);
}

View File

@ -48,6 +48,7 @@ import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import {
getEntityDetailsPath,
getVersionPath,
ROUTES,
} from '../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../constants/entity.constants';
import { mockDatasetData } from '../../constants/mockTourData.constants';
@ -58,6 +59,7 @@ import {
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { useTourProvider } from '../../context/TourProvider/TourProvider';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import {
EntityTabs,
@ -196,7 +198,9 @@ const TableDetailsPageV1: React.FC = () => {
id: details.id,
});
} catch (error) {
// Error here
if ((error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN) {
history.replace(ROUTES.FORBIDDEN);
}
} finally {
setLoading(false);
}

View File

@ -27,12 +27,13 @@ import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/Error
import Loader from '../../components/common/Loader/Loader';
import { QueryVote } from '../../components/Database/TableQueries/TableQueries.interface';
import TopicDetails from '../../components/Topic/TopicDetails/TopicDetails.component';
import { getVersionPath } from '../../constants/constants';
import { getVersionPath, ROUTES } from '../../constants/constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
@ -152,6 +153,10 @@ const TopicDetailsPage: FunctionComponent = () => {
} catch (error) {
if ((error as AxiosError).response?.status === 404) {
setIsError(true);
} else if (
(error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN
) {
history.replace(ROUTES.FORBIDDEN);
} else {
showErrorToast(
error as AxiosError,