Tests #19066: Playwright coverage for ViewAll rule with matchAnyTag() and isOwner() condition (#19374)

* Modify the setup for the tests and add teardown to reset the organization policies

* Fix the loader shown in case of no permission

* Add playwright tests to cover the viewAll permission with conditions

* Add description for the commented code
This commit is contained in:
Aniket Katkar 2025-01-17 09:08:54 +05:30 committed by GitHub
parent d8e6219200
commit 7fea955338
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 901 additions and 28 deletions

View File

@ -63,6 +63,11 @@ export default defineConfig({
{
name: 'setup',
testMatch: '**/*.setup.ts',
teardown: 'restore-policies',
},
{
name: 'restore-policies',
testMatch: '**/auth.teardown.ts',
},
{
name: 'chromium',

View File

@ -0,0 +1,175 @@
/*
* Copyright 2025 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 { PolicyClass } from '../support/access-control/PoliciesClass';
import { RolesClass } from '../support/access-control/RolesClass';
import { ApiCollectionClass } from '../support/entity/ApiCollectionClass';
import { ContainerClass } from '../support/entity/ContainerClass';
import { DashboardClass } from '../support/entity/DashboardClass';
import { MlModelClass } from '../support/entity/MlModelClass';
import { PipelineClass } from '../support/entity/PipelineClass';
import { SearchIndexClass } from '../support/entity/SearchIndexClass';
import { TableClass } from '../support/entity/TableClass';
import { TopicClass } from '../support/entity/TopicClass';
import { EntityData } from '../support/interfaces/ConditionalPermissions.interface';
import { UserClass } from '../support/user/UserClass';
import { ServiceTypes } from './settings';
export const isOwnerPolicy = new PolicyClass();
export const matchAnyTagPolicy = new PolicyClass();
export const isOwnerRole = new RolesClass();
export const matchAnyTagRole = new RolesClass();
export const userWithOwnerPermission = new UserClass();
export const userWithTagPermission = new UserClass();
export const apiCollectionWithOwner = new ApiCollectionClass();
export const apiCollectionWithTag = new ApiCollectionClass();
export const containerWithOwner = new ContainerClass();
export const containerWithTag = new ContainerClass();
export const dashboardWithOwner = new DashboardClass();
export const dashboardWithTag = new DashboardClass();
export const mlModelWithOwner = new MlModelClass();
export const mlModelWithTag = new MlModelClass();
export const pipelineWithOwner = new PipelineClass();
export const pipelineWithTag = new PipelineClass();
export const searchIndexWithOwner = new SearchIndexClass();
export const searchIndexWithTag = new SearchIndexClass();
export const tableWithOwner = new TableClass();
export const tableWithTag = new TableClass();
export const topicWithOwner = new TopicClass();
export const topicWithTag = new TopicClass();
const withOwner = {
apiCollectionWithOwner,
containerWithOwner,
dashboardWithOwner,
mlModelWithOwner,
pipelineWithOwner,
searchIndexWithOwner,
tableWithOwner,
topicWithOwner,
};
const withTag = {
apiCollectionWithTag,
containerWithTag,
dashboardWithTag,
mlModelWithTag,
pipelineWithTag,
searchIndexWithTag,
tableWithTag,
topicWithTag,
};
export const assetsData = [
{
asset: ServiceTypes.API_SERVICES,
withOwner: apiCollectionWithOwner,
withTag: apiCollectionWithTag,
childTabId: 'collections',
assetOwnerUrl: `/service/${apiCollectionWithOwner.serviceType}`,
assetTagUrl: `/service/${apiCollectionWithTag.serviceType}`,
},
{
asset: ServiceTypes.STORAGE_SERVICES,
withOwner: containerWithOwner,
withTag: containerWithTag,
childTabId: 'containers',
assetOwnerUrl: `/service/${containerWithOwner.serviceType}`,
assetTagUrl: `/service/${containerWithTag.serviceType}`,
},
{
asset: ServiceTypes.DASHBOARD_SERVICES,
withOwner: dashboardWithOwner,
withTag: dashboardWithTag,
childTabId: 'dashboards',
childTabId2: 'data-model',
childTableId2: 'data-models-table',
assetOwnerUrl: `/service/${dashboardWithOwner.serviceType}`,
assetTagUrl: `/service/${dashboardWithTag.serviceType}`,
},
{
asset: ServiceTypes.ML_MODEL_SERVICES,
withOwner: mlModelWithOwner,
withTag: mlModelWithTag,
childTabId: 'ml models',
assetOwnerUrl: `/service/${mlModelWithOwner.serviceType}`,
assetTagUrl: `/service/${mlModelWithTag.serviceType}`,
},
{
asset: ServiceTypes.PIPELINE_SERVICES,
withOwner: pipelineWithOwner,
withTag: pipelineWithTag,
childTabId: 'pipelines',
assetOwnerUrl: `/service/${pipelineWithOwner.serviceType}`,
assetTagUrl: `/service/${pipelineWithTag.serviceType}`,
},
// TODO: Uncomment when search index permission issue is fixed
// {
// asset: ServiceTypes.SEARCH_SERVICES,
// withOwner: searchIndexWithOwner,
// withTag: searchIndexWithTag,
// childTabId: 'search indexes',
// assetOwnerUrl: `/service/${searchIndexWithOwner.serviceType}`,
// assetTagUrl: `/service/${searchIndexWithTag.serviceType}`,
// },
{
asset: ServiceTypes.DATABASE_SERVICES,
withOwner: tableWithOwner,
withTag: tableWithTag,
childTabId: 'databases',
assetOwnerUrl: `/service/${tableWithOwner.serviceType}`,
assetTagUrl: `/service/${tableWithTag.serviceType}`,
},
{
asset: ServiceTypes.MESSAGING_SERVICES,
withOwner: topicWithOwner,
withTag: topicWithTag,
childTabId: 'topics',
assetOwnerUrl: `/service/${topicWithOwner.serviceType}`,
assetTagUrl: `/service/${topicWithTag.serviceType}`,
},
{
asset: 'database',
withOwner: tableWithOwner,
withTag: tableWithTag,
childTabId: 'schema',
assetOwnerUrl: `/database`,
assetTagUrl: `/database`,
},
{
asset: 'databaseSchema',
withOwner: tableWithOwner,
withTag: tableWithTag,
childTabId: 'table',
assetOwnerUrl: `/databaseSchema`,
assetTagUrl: `/databaseSchema`,
},
{
asset: 'container',
withOwner: containerWithOwner,
withTag: containerWithTag,
childTabId: 'children',
assetOwnerUrl: `/container`,
assetTagUrl: `/container`,
},
];
export const conditionalPermissionsEntityData: EntityData = {
isOwnerPolicy,
matchAnyTagPolicy,
isOwnerRole,
matchAnyTagRole,
userWithOwnerPermission,
userWithTagPermission,
withOwner,
withTag,
};

View File

@ -77,6 +77,36 @@ export const DATA_CONSUMER_RULES: PolicyRulesType[] = [
},
];
export const VIEW_ALL_RULE: PolicyRulesType[] = [
{
name: 'OrganizationPolicy-ViewAll-Rule',
description: 'Allow all users to view all metadata',
resources: ['All'],
operations: ['ViewAll'],
effect: 'allow',
},
];
export const VIEW_ALL_WITH_IS_OWNER: PolicyRulesType[] = [
{
name: 'viewAll-IsOwner',
resources: ['All'],
operations: ['ViewAll'],
effect: 'allow',
condition: 'isOwner()',
},
];
export const VIEW_ALL_WITH_MATCH_TAG_CONDITION: PolicyRulesType[] = [
{
name: 'viewAll-MatchTag',
resources: ['All'],
operations: ['ViewAll'],
effect: 'allow',
condition: "matchAnyTag('PersonalData.Personal')",
},
];
export const EDIT_USER_FOR_TEAM_RULES: PolicyRulesType[] = [
{
name: 'EditUserTeams-EditRule',
@ -104,13 +134,6 @@ export const ORGANIZATION_POLICY_RULES: PolicyRulesType[] = [
resources: ['All'],
condition: 'isOwner()',
},
{
name: 'OrganizationPolicy-ViewAll-Rule',
description: 'Allow all users to discover data assets.',
effect: 'allow',
operations: ['ViewAll'],
resources: ['All'],
},
];
export const GLOBAL_SETTING_PERMISSIONS: Record<

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { uuid } from '../utils/common';
import { GlobalSettingOptions } from './settings';
import { GlobalSettingOptions, ServiceTypes } from './settings';
export const SERVICE_TYPE = {
Database: GlobalSettingOptions.DATABASES,
@ -26,49 +26,38 @@ export const SERVICE_TYPE = {
ApiService: GlobalSettingOptions.APIS,
};
export const SERVICE_CATEGORIES = {
DATABASE_SERVICES: 'databaseServices',
MESSAGING_SERVICES: 'messagingServices',
PIPELINE_SERVICES: 'pipelineServices',
DASHBOARD_SERVICES: 'dashboardServices',
ML_MODEL_SERVICES: 'mlmodelServices',
STORAGE_SERVICES: 'storageServices',
METADATA_SERVICES: 'metadataServices',
SEARCH_SERVICES: 'searchServices',
};
export const VISIT_SERVICE_PAGE_DETAILS = {
[SERVICE_TYPE.Database]: {
settingsMenuId: GlobalSettingOptions.DATABASES,
serviceCategory: SERVICE_CATEGORIES.DATABASE_SERVICES,
serviceCategory: ServiceTypes.DATABASE_SERVICES,
},
[SERVICE_TYPE.Messaging]: {
settingsMenuId: GlobalSettingOptions.MESSAGING,
serviceCategory: SERVICE_CATEGORIES.MESSAGING_SERVICES,
serviceCategory: ServiceTypes.MESSAGING_SERVICES,
},
[SERVICE_TYPE.Dashboard]: {
settingsMenuId: GlobalSettingOptions.DASHBOARDS,
serviceCategory: SERVICE_CATEGORIES.DASHBOARD_SERVICES,
serviceCategory: ServiceTypes.DASHBOARD_SERVICES,
},
[SERVICE_TYPE.Pipeline]: {
settingsMenuId: GlobalSettingOptions.PIPELINES,
serviceCategory: SERVICE_CATEGORIES.PIPELINE_SERVICES,
serviceCategory: ServiceTypes.PIPELINE_SERVICES,
},
[SERVICE_TYPE.MLModels]: {
settingsMenuId: GlobalSettingOptions.MLMODELS,
serviceCategory: SERVICE_CATEGORIES.ML_MODEL_SERVICES,
serviceCategory: ServiceTypes.ML_MODEL_SERVICES,
},
[SERVICE_TYPE.Storage]: {
settingsMenuId: GlobalSettingOptions.STORAGES,
serviceCategory: SERVICE_CATEGORIES.STORAGE_SERVICES,
serviceCategory: ServiceTypes.STORAGE_SERVICES,
},
[SERVICE_TYPE.Search]: {
settingsMenuId: GlobalSettingOptions.SEARCH,
serviceCategory: SERVICE_CATEGORIES.SEARCH_SERVICES,
serviceCategory: ServiceTypes.SEARCH_SERVICES,
},
[SERVICE_TYPE.Metadata]: {
settingsMenuId: GlobalSettingOptions.METADATA,
serviceCategory: SERVICE_CATEGORIES.METADATA_SERVICES,
serviceCategory: ServiceTypes.METADATA_SERVICES,
},
};

View File

@ -21,6 +21,18 @@ export enum GlobalSettingsMenuCategory {
APPLICATIONS = 'apps',
}
export enum ServiceTypes {
API_SERVICES = 'apiServices',
DATABASE_SERVICES = 'databaseServices',
MESSAGING_SERVICES = 'messagingServices',
PIPELINE_SERVICES = 'pipelineServices',
DASHBOARD_SERVICES = 'dashboardServices',
ML_MODEL_SERVICES = 'mlmodelServices',
STORAGE_SERVICES = 'storageServices',
METADATA_SERVICES = 'metadataServices',
SEARCH_SERVICES = 'searchServices',
}
export enum GlobalSettingOptions {
USERS = 'users',
ADMINS = 'admins',
@ -80,6 +92,10 @@ export enum GlobalSettingOptions {
export const SETTINGS_OPTIONS_PATH = {
// Services
[GlobalSettingOptions.API_COLLECTIONS]: [
GlobalSettingsMenuCategory.SERVICES,
`${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.API_COLLECTIONS}`,
],
[GlobalSettingOptions.DATABASES]: [
GlobalSettingsMenuCategory.SERVICES,
`${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.DATABASES}`,

View File

@ -0,0 +1,113 @@
/*
* Copyright 2025 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 { Page, test as base } from '@playwright/test';
import { startCase } from 'lodash';
import {
assetsData,
userWithOwnerPermission,
userWithTagPermission,
} from '../../constant/conditionalPermissions';
import { performAdminLogin } from '../../utils/admin';
import {
checkViewAllPermission,
conditionalPermissionsCleanup,
conditionalPermissionsPrerequisites,
getEntityFQN,
} from '../../utils/conditionalPermissions';
const test = base.extend<{
user1Page: Page;
user2Page: Page;
}>({
user1Page: async ({ browser }, use) => {
const page = await browser.newPage();
await userWithOwnerPermission.login(page);
await use(page);
await page.close();
},
user2Page: async ({ browser }, use) => {
const page = await browser.newPage();
await userWithTagPermission.login(page);
await use(page);
await page.close();
},
});
test.beforeAll(async ({ browser }) => {
test.slow();
const { apiContext, afterAction } = await performAdminLogin(browser);
await conditionalPermissionsPrerequisites(apiContext);
await afterAction();
});
test.afterAll(async ({ browser }) => {
test.slow();
const { apiContext, afterAction } = await performAdminLogin(browser);
await conditionalPermissionsCleanup(apiContext);
await afterAction();
});
for (const serviceData of assetsData) {
const {
asset,
withOwner,
withTag,
assetOwnerUrl,
assetTagUrl,
childTabId,
childTabId2,
childTableId2,
} = serviceData;
test(`User with owner permission can only view owned ${startCase(
asset
)}`, async ({ user1Page: page }) => {
// Get the FQNs of both assets
const ownerAssetName = getEntityFQN(asset, withOwner);
const tagAssetName = getEntityFQN(asset, withTag);
const ownerAssetURL = `${assetOwnerUrl}/${ownerAssetName}`;
const tagAssetURL = `${assetTagUrl}/${tagAssetName}`;
await checkViewAllPermission({
page,
url1: ownerAssetURL,
url2: tagAssetURL,
childTabId,
childTabId2,
childTableId2,
});
});
test(`User with matchAnyTag permission can only view ${startCase(
asset
)} with the tag`, async ({ user2Page: page }) => {
// Get the FQNs of both assets
const ownerAssetName = getEntityFQN(asset, withOwner);
const tagAssetName = getEntityFQN(asset, withTag);
const ownerAssetURL = `${assetOwnerUrl}/${ownerAssetName}`;
const tagAssetURL = `${assetTagUrl}/${tagAssetName}`;
await checkViewAllPermission({
page,
url1: tagAssetURL,
url2: ownerAssetURL,
childTabId,
childTabId2,
childTableId2,
});
});
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2025 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 { test as teardown } from '@playwright/test';
import { AdminClass } from '../support/user/AdminClass';
import { resetPolicyChanges } from '../utils/authTeardown';
teardown('restore the organization roles and policies', async ({ page }) => {
const admin = new AdminClass();
// Reset the default organization roles and policies
await resetPolicyChanges(page, admin);
});

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import { visitServiceDetailsPage } from '../../utils/service';
@ -149,6 +150,8 @@ export class ApiCollectionClass extends EntityClass {
constructor(name?: string) {
super(EntityTypeEndpoint.API_COLLECTION);
this.serviceCategory = SERVICE_TYPE.ApiService;
this.serviceType = ServiceTypes.API_SERVICES;
this.service.name = name ?? this.service.name;
this.type = 'Api Collection';
}

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -153,6 +154,7 @@ export class ApiEndpointClass extends EntityClass {
super(EntityTypeEndpoint.API_ENDPOINT);
this.service.name = name ?? this.service.name;
this.serviceCategory = SERVICE_TYPE.ApiService;
this.serviceType = ServiceTypes.API_SERVICES;
this.type = 'ApiEndpoint';
this.childrenTabId = 'schema';
this.childrenSelectorId = this.children[0].name;

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -24,6 +25,7 @@ import { EntityClass } from './EntityClass';
export class ContainerClass extends EntityClass {
private containerName = `pw-container-${uuid()}`;
private childContainerName = `pw-container-${uuid()}`;
service = {
name: `pw-storage-service-${uuid()}`,
serviceType: 'S3',
@ -45,14 +47,21 @@ export class ContainerClass extends EntityClass {
displayName: this.containerName,
service: this.service.name,
};
childContainer = {
name: this.childContainerName,
displayName: this.childContainerName,
service: this.service.name,
};
serviceResponseData: ResponseDataType = {} as ResponseDataType;
entityResponseData: ResponseDataWithServiceType =
{} as ResponseDataWithServiceType;
childResponseData: ResponseDataType = {} as ResponseDataType;
constructor(name?: string) {
super(EntityTypeEndpoint.Container);
this.service.name = name ?? this.service.name;
this.serviceType = ServiceTypes.STORAGE_SERVICES;
this.type = 'Container';
this.serviceCategory = SERVICE_TYPE.Storage;
}
@ -71,6 +80,20 @@ export class ContainerClass extends EntityClass {
this.serviceResponseData = await serviceResponse.json();
this.entityResponseData = await entityResponse.json();
const childContainer = {
...this.childContainer,
parent: {
id: this.entityResponseData.id,
type: 'container',
},
};
const childResponse = await apiContext.post('/api/v1/containers', {
data: childContainer,
});
this.childResponseData = await childResponse.json();
return {
service: serviceResponse.body,
entity: entityResponse.body,

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -24,6 +25,7 @@ import { EntityClass } from './EntityClass';
export class DashboardClass extends EntityClass {
private dashboardName = `pw-dashboard-${uuid()}`;
private dashboardDataModelName = `pw-dashboard-data-model-${uuid()}`;
service = {
name: `pw-dashboard-service-${uuid()}`,
serviceType: 'Superset',
@ -50,10 +52,28 @@ export class DashboardClass extends EntityClass {
displayName: this.dashboardName,
service: this.service.name,
};
children = [
{
name: 'country_name',
dataType: 'VARCHAR',
dataLength: 256,
dataTypeDisplay: 'varchar',
description: 'Name of the country.',
},
];
dataModel = {
name: this.dashboardDataModelName,
displayName: this.dashboardDataModelName,
service: this.service.name,
columns: this.children,
dataModelType: 'SupersetDataModel',
};
serviceResponseData: ResponseDataType = {} as ResponseDataType;
entityResponseData: ResponseDataWithServiceType =
{} as ResponseDataWithServiceType;
dataModelResponseData: ResponseDataWithServiceType =
{} as ResponseDataWithServiceType;
chartsResponseData: ResponseDataType = {} as ResponseDataType;
constructor(name?: string) {
@ -61,6 +81,7 @@ export class DashboardClass extends EntityClass {
this.service.name = name ?? this.service.name;
this.type = 'Dashboard';
this.serviceCategory = SERVICE_TYPE.Dashboard;
this.serviceType = ServiceTypes.DASHBOARD_SERVICES;
}
async create(apiContext: APIRequestContext) {
@ -80,9 +101,16 @@ export class DashboardClass extends EntityClass {
charts: [`${this.service.name}.${this.charts.name}`],
},
});
const dataModelResponse = await apiContext.post(
'/api/v1/dashboard/datamodels',
{
data: this.dataModel,
}
);
this.serviceResponseData = await serviceResponse.json();
this.chartsResponseData = await chartsResponse.json();
this.dataModelResponseData = await dataModelResponse.json();
this.entityResponseData = await entityResponse.json();
return {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -70,6 +71,7 @@ export class DashboardDataModelClass extends EntityClass {
this.childrenTabId = 'model';
this.childrenSelectorId = this.children[0].name;
this.serviceCategory = SERVICE_TYPE.Dashboard;
this.serviceType = ServiceTypes.DASHBOARD_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, expect, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import {
assignDomain,
removeDomain,
@ -127,6 +128,7 @@ export class DatabaseClass extends EntityClass {
super(EntityTypeEndpoint.Database);
this.service.name = name ?? this.service.name;
this.type = 'Database';
this.serviceType = ServiceTypes.DATABASE_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitServiceDetailsPage } from '../../utils/service';
import {
@ -61,6 +62,7 @@ export class DatabaseSchemaClass extends EntityClass {
super(EntityTypeEndpoint.DatabaseSchema);
this.service.name = name ?? this.service.name;
this.type = 'Database Schema';
this.serviceType = ServiceTypes.DATABASE_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -12,7 +12,7 @@
*/
import { APIRequestContext, Page } from '@playwright/test';
import { CustomPropertySupportedEntityList } from '../../constant/customProperty';
import { GlobalSettingOptions } from '../../constant/settings';
import { GlobalSettingOptions, ServiceTypes } from '../../constant/settings';
import { assignDomain, removeDomain, updateDomain } from '../../utils/common';
import {
createCustomPropertyForEntity,
@ -56,6 +56,7 @@ import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface';
export class EntityClass {
type = '';
serviceCategory?: GlobalSettingOptions;
serviceType?: ServiceTypes;
childrenTabId?: string;
childrenSelectorId?: string;
endpoint: EntityTypeEndpoint;

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -64,6 +65,7 @@ export class MlModelClass extends EntityClass {
this.childrenTabId = 'features';
this.childrenSelectorId = `feature-card-${this.children[0].name}`;
this.serviceCategory = SERVICE_TYPE.MLModels;
this.serviceType = ServiceTypes.ML_MODEL_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -59,6 +60,7 @@ export class PipelineClass extends EntityClass {
this.childrenTabId = 'tasks';
this.childrenSelectorId = this.children[0].name;
this.serviceCategory = SERVICE_TYPE.Pipeline;
this.serviceType = ServiceTypes.PIPELINE_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -105,6 +106,7 @@ export class SearchIndexClass extends EntityClass {
this.childrenTabId = 'fields';
this.childrenSelectorId = this.children[0].fullyQualifiedName;
this.serviceCategory = SERVICE_TYPE.Search;
this.serviceType = ServiceTypes.SEARCH_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -71,6 +72,7 @@ export class StoredProcedureClass extends EntityClass {
this.service.name = name ?? this.service.name;
this.serviceCategory = SERVICE_TYPE.Database;
this.type = 'Store Procedure';
this.serviceType = ServiceTypes.DATABASE_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -14,6 +14,7 @@ import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -125,6 +126,7 @@ export class TableClass extends EntityClass {
super(EntityTypeEndpoint.Table);
this.service.name = name ?? this.service.name;
this.serviceCategory = SERVICE_TYPE.Database;
this.serviceType = ServiceTypes.DATABASE_SERVICES;
this.type = 'Table';
this.childrenTabId = 'schema';
this.childrenSelectorId = `${this.entity.databaseSchema}.${this.entity.name}.${this.children[0].name}`;

View File

@ -13,6 +13,7 @@
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
import { uuid } from '../../utils/common';
import { visitEntityPage } from '../../utils/entity';
import {
@ -107,6 +108,7 @@ export class TopicClass extends EntityClass {
this.childrenTabId = 'schema';
this.childrenSelectorId = this.children[0].name;
this.serviceCategory = SERVICE_TYPE.Messaging;
this.serviceType = ServiceTypes.MESSAGING_SERVICES;
}
async create(apiContext: APIRequestContext) {

View File

@ -0,0 +1,61 @@
/*
* Copyright 2025 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 { PolicyClass } from '../access-control/PoliciesClass';
import { RolesClass } from '../access-control/RolesClass';
import { ApiCollectionClass } from '../entity/ApiCollectionClass';
import { ContainerClass } from '../entity/ContainerClass';
import { DashboardClass } from '../entity/DashboardClass';
import { MlModelClass } from '../entity/MlModelClass';
import { PipelineClass } from '../entity/PipelineClass';
import { SearchIndexClass } from '../entity/SearchIndexClass';
import { TableClass } from '../entity/TableClass';
import { TopicClass } from '../entity/TopicClass';
import { UserClass } from '../user/UserClass';
export interface EntityData {
isOwnerPolicy: PolicyClass;
matchAnyTagPolicy: PolicyClass;
isOwnerRole: RolesClass;
matchAnyTagRole: RolesClass;
userWithOwnerPermission: UserClass;
userWithTagPermission: UserClass;
withOwner: {
apiCollectionWithOwner: ApiCollectionClass;
containerWithOwner: ContainerClass;
dashboardWithOwner: DashboardClass;
mlModelWithOwner: MlModelClass;
pipelineWithOwner: PipelineClass;
searchIndexWithOwner: SearchIndexClass;
tableWithOwner: TableClass;
topicWithOwner: TopicClass;
};
withTag: {
apiCollectionWithTag: ApiCollectionClass;
containerWithTag: ContainerClass;
dashboardWithTag: DashboardClass;
mlModelWithTag: MlModelClass;
pipelineWithTag: PipelineClass;
searchIndexWithTag: SearchIndexClass;
tableWithTag: TableClass;
topicWithTag: TopicClass;
};
}
export type AssetTypes =
| ApiCollectionClass
| ContainerClass
| DashboardClass
| MlModelClass
| PipelineClass
| TableClass
| TopicClass;

View File

@ -0,0 +1,88 @@
/*
* Copyright 2025 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 { APIRequestContext, Page } from '@playwright/test';
import {
ORGANIZATION_POLICY_RULES,
VIEW_ALL_RULE,
} from '../constant/permission';
import { AdminClass } from '../support/user/AdminClass';
import { getApiContext } from './common';
export const restoreOrganizationDefaultRole = async (
apiContext: APIRequestContext
) => {
const organizationTeamResponse = await apiContext
.get(`/api/v1/teams/name/Organization`)
.then((res) => res.json());
const dataConsumerRoleResponse = await apiContext
.get('/api/v1/roles/name/DataConsumer')
.then((res) => res.json());
await apiContext.patch(`/api/v1/teams/${organizationTeamResponse.id}`, {
data: [
{
op: 'replace',
path: '/defaultRoles',
value: [
{
id: dataConsumerRoleResponse.id,
type: 'role',
},
],
},
],
headers: {
'Content-Type': 'application/json-patch+json',
},
});
};
export const updateDefaultOrganizationPolicy = async (
apiContext: APIRequestContext
) => {
const orgPolicyResponse = await apiContext
.get('/api/v1/policies/name/OrganizationPolicy')
.then((response) => response.json());
await apiContext.patch(`/api/v1/policies/${orgPolicyResponse.id}`, {
data: [
{
op: 'replace',
path: '/rules',
value: [...ORGANIZATION_POLICY_RULES, ...VIEW_ALL_RULE],
},
],
headers: {
'Content-Type': 'application/json-patch+json',
},
});
};
const restoreRolesAndPolicies = async (page: Page) => {
const { apiContext, afterAction } = await getApiContext(page);
// Remove organization policy and role
await restoreOrganizationDefaultRole(apiContext);
// update default Organization policy
await updateDefaultOrganizationPolicy(apiContext);
await afterAction();
};
export const resetPolicyChanges = async (page: Page, admin: AdminClass) => {
await admin.login(page);
await page.waitForURL('**/my-data');
await restoreRolesAndPolicies(page);
await admin.logout(page);
await page.waitForURL('**/signin');
};

View File

@ -0,0 +1,304 @@
/*
* Copyright 2025 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 { APIRequestContext, expect, Page } from '@playwright/test';
import { conditionalPermissionsEntityData } from '../constant/conditionalPermissions';
import {
VIEW_ALL_WITH_IS_OWNER,
VIEW_ALL_WITH_MATCH_TAG_CONDITION,
} from '../constant/permission';
import { TableClass } from '../support/entity/TableClass';
import { AssetTypes } from '../support/interfaces/ConditionalPermissions.interface';
import { redirectToHomePage } from './common';
export const conditionalPermissionsPrerequisites = async (
apiContext: APIRequestContext
) => {
const {
isOwnerPolicy,
matchAnyTagPolicy,
isOwnerRole,
matchAnyTagRole,
userWithOwnerPermission,
userWithTagPermission,
withOwner,
withTag,
} = conditionalPermissionsEntityData;
await userWithOwnerPermission.create(apiContext);
await userWithTagPermission.create(apiContext);
await withOwner.apiCollectionWithOwner.create(apiContext);
await withTag.apiCollectionWithTag.create(apiContext);
await withOwner.containerWithOwner.create(apiContext);
await withTag.containerWithTag.create(apiContext);
await withOwner.dashboardWithOwner.create(apiContext);
await withTag.dashboardWithTag.create(apiContext);
await withOwner.mlModelWithOwner.create(apiContext);
await withTag.mlModelWithTag.create(apiContext);
await withOwner.pipelineWithOwner.create(apiContext);
await withTag.pipelineWithTag.create(apiContext);
await withOwner.searchIndexWithOwner.create(apiContext);
await withTag.searchIndexWithTag.create(apiContext);
await withOwner.tableWithOwner.create(apiContext);
await withTag.tableWithTag.create(apiContext);
await withOwner.topicWithOwner.create(apiContext);
await withTag.topicWithTag.create(apiContext);
const isOwnerPolicyResponse = await isOwnerPolicy.create(
apiContext,
VIEW_ALL_WITH_IS_OWNER
);
const matchAnyTagPolicyResponse = await matchAnyTagPolicy.create(
apiContext,
VIEW_ALL_WITH_MATCH_TAG_CONDITION
);
const isOwnerRoleResponse = await isOwnerRole.create(apiContext, [
isOwnerPolicyResponse.fullyQualifiedName,
]);
const matchAnyTagRoleResponse = await matchAnyTagRole.create(apiContext, [
matchAnyTagPolicyResponse.fullyQualifiedName,
]);
await userWithOwnerPermission.patch({
apiContext,
patchData: [
{
op: 'replace',
path: '/roles',
value: [
{
id: isOwnerRoleResponse.id,
type: 'role',
name: isOwnerRoleResponse.name,
},
],
},
],
});
await userWithTagPermission.patch({
apiContext,
patchData: [
{
op: 'replace',
path: '/roles',
value: [
{
id: matchAnyTagRoleResponse.id,
type: 'role',
name: matchAnyTagRoleResponse.name,
},
],
},
],
});
const ownerPatchData = {
data: [
{
op: 'replace',
path: '/owners',
value: [
{
id: userWithOwnerPermission.responseData.id,
type: 'user',
},
],
},
],
headers: {
'Content-Type': 'application/json-patch+json',
},
};
const tagPatchData = {
data: [
{
op: 'replace',
path: '/tags',
value: [
{
tagFQN: 'PersonalData.Personal',
source: 'Classification',
labelType: 'Manual',
name: 'Personal',
state: 'Confirmed',
},
],
},
],
headers: {
'Content-Type': 'application/json-patch+json',
},
};
for (const entity of Object.values(withOwner)) {
await apiContext.patch(
`/api/v1/services/${entity.serviceType}/${entity.serviceResponseData.id}`,
ownerPatchData
);
}
await apiContext.patch(
`/api/v1/databases/${withOwner.tableWithOwner.databaseResponseData.id}`,
ownerPatchData
);
await apiContext.patch(
`/api/v1/databaseSchemas/${withOwner.tableWithOwner.schemaResponseData.id}`,
ownerPatchData
);
await apiContext.patch(
`/api/v1/containers/${withOwner.containerWithOwner.entityResponseData.id}`,
ownerPatchData
);
for (const entity of Object.values(withTag)) {
await apiContext.patch(
`/api/v1/services/${entity.serviceType}/${entity.serviceResponseData.id}`,
tagPatchData
);
}
await apiContext.patch(
`/api/v1/databases/${withTag.tableWithTag.databaseResponseData.id}`,
tagPatchData
);
await apiContext.patch(
`/api/v1/databaseSchemas/${withTag.tableWithTag.schemaResponseData.id}`,
tagPatchData
);
await apiContext.patch(
`/api/v1/containers/${withTag.containerWithTag.entityResponseData.id}`,
tagPatchData
);
};
export const conditionalPermissionsCleanup = async (
apiContext: APIRequestContext
) => {
const {
isOwnerPolicy,
matchAnyTagPolicy,
isOwnerRole,
matchAnyTagRole,
userWithOwnerPermission,
userWithTagPermission,
withOwner: {
apiCollectionWithOwner,
containerWithOwner,
dashboardWithOwner,
mlModelWithOwner,
pipelineWithOwner,
searchIndexWithOwner,
tableWithOwner,
topicWithOwner,
},
withTag: {
apiCollectionWithTag,
containerWithTag,
dashboardWithTag,
mlModelWithTag,
pipelineWithTag,
searchIndexWithTag,
tableWithTag,
topicWithTag,
},
} = conditionalPermissionsEntityData;
await isOwnerRole.delete(apiContext);
await matchAnyTagRole.delete(apiContext);
await isOwnerPolicy.delete(apiContext);
await matchAnyTagPolicy.delete(apiContext);
await userWithOwnerPermission.delete(apiContext);
await userWithTagPermission.delete(apiContext);
await apiCollectionWithOwner.delete(apiContext);
await apiCollectionWithTag.delete(apiContext);
await containerWithOwner.delete(apiContext);
await containerWithTag.delete(apiContext);
await dashboardWithOwner.delete(apiContext);
await dashboardWithTag.delete(apiContext);
await mlModelWithOwner.delete(apiContext);
await mlModelWithTag.delete(apiContext);
await pipelineWithOwner.delete(apiContext);
await pipelineWithTag.delete(apiContext);
await searchIndexWithOwner.delete(apiContext);
await searchIndexWithTag.delete(apiContext);
await tableWithOwner.delete(apiContext);
await tableWithTag.delete(apiContext);
await topicWithOwner.delete(apiContext);
await topicWithTag.delete(apiContext);
};
export const getEntityFQN = (assetName: string, asset: AssetTypes) => {
if (assetName === 'database') {
return (asset as TableClass).databaseResponseData.fullyQualifiedName;
}
if (assetName === 'databaseSchema') {
return (asset as TableClass).schemaResponseData.fullyQualifiedName;
}
if (assetName === 'container') {
return asset.entityResponseData.fullyQualifiedName;
}
return asset.serviceResponseData.fullyQualifiedName;
};
export const checkViewAllPermission = async ({
page,
url1,
url2,
childTabId,
childTabId2,
childTableId2,
}: {
page: Page;
url1: string;
url2: string;
childTabId: string;
childTabId2?: string;
childTableId2?: string;
}) => {
await redirectToHomePage(page);
// visit the page of the asset with permission
await page.goto(url1);
// Check if the details are shown properly
await expect(page.getByTestId('data-assets-header')).toBeAttached();
await page.waitForSelector(`[data-testid="${childTabId}"]`);
await page.click(`[data-testid="${childTabId}"]`);
await expect(
page
.getByTestId('service-children-table')
.getByTestId('no-data-placeholder')
).not.toBeAttached();
if (childTabId2) {
await page.click(`[data-testid="${childTabId2}"]`);
await expect(
page.getByTestId(childTableId2 ?? '').getByTestId('no-data-placeholder')
).not.toBeAttached();
}
// visit the page of the asset without permission
await page.goto(url2);
// Check if the no permissions placeholder is shown
await expect(page.getByTestId('permission-error-placeholder')).toBeAttached();
};

View File

@ -355,6 +355,8 @@ const DatabaseDetails: FunctionComponent = () => {
if (databasePermission.ViewAll || databasePermission.ViewBasic) {
getDetailsByFQN();
fetchDatabaseSchemaCount();
} else {
setIsDatabaseDetailsLoading(false);
}
}, [databasePermission, decodedDatabaseFQN]);