Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-bulk-import-common': minor
'@red-hat-developer-hub/backstage-plugin-bulk-import': minor
'@red-hat-developer-hub/backstage-plugin-bulk-import-backend': minor
---

Convert `bulk.import` from a resource permission to a feature permission, and register the NFS bulk import page using the `if` permission predicate.
3 changes: 1 addition & 2 deletions workspaces/bulk-import/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,7 @@ catalog:

# see https://backstage.io/docs/permissions/getting-started for more on the permission framework
permission:
# setting this to `false` will disable permissions
enabled: false
enabled: true
rbac:
# Paths are relative to packages/backend (yarn start cwd).
policies-csv-file: ../../examples/rbac-policy.csv
Expand Down
32 changes: 11 additions & 21 deletions workspaces/bulk-import/e2e-tests/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import {
PreviewSidebarSnapshotsType,
} from './utils/ariaSnapshots';
import { runAccessibilityTests, switchToLocale } from './utils/helpers';
import { installMockBulkImportPermission } from './utils/permissionUtils';
import {
BulkImportMessages,
getBulkImportNavLabel,
getSelectedRepositoriesHeading,
getTranslations,
} from './utils/translations';
Expand Down Expand Up @@ -70,6 +72,7 @@ test.describe('Bulk Import', () => {
//
// This lets us focus on UI behaviour without needing a real OAuth provider
// set up in the test environment.
await installMockBulkImportPermission(sharedPage, 'ALLOW');
await mockBulkImportSCMHostsResponse(sharedPage, mockSCMHostsData);
await mockBulkImportRepositoriesResponse(sharedPage, mockRepositoriesData);
await sharedPage.goto('/');
Expand All @@ -78,11 +81,6 @@ test.describe('Bulk Import', () => {
await expect(enterButton).toBeVisible();
await enterButton.click();

// Wait for authentication to complete - wait for sidebar or main content to appear
await sharedPage.waitForLoadState('networkidle');
// Additional wait to ensure auth state is fully initialized
await sharedPage.waitForTimeout(500);

const currentLocale = await sharedPage.evaluate(
() => globalThis.navigator.language,
);
Expand Down Expand Up @@ -111,7 +109,9 @@ test.describe('Bulk Import', () => {
}

await sharedPage.reload();
await sharedPage.waitForLoadState('networkidle');
await expect(sharedPage.getByText('All Components')).toBeVisible({
timeout: 30000,
});
}
}

Expand All @@ -120,21 +120,11 @@ test.describe('Bulk Import', () => {
translations = getTranslations(currentLocale);
previewSidebarSnapshots = getPreviewSidebarSnapshots(translations);

// Sidebar text is not yet getting translated and will be covered as part of story https://issues.redhat.com/browse/RHIDP-12094.
// TODO: Revert the change once the story is resolved.
if (process.env.APP_MODE === 'legacy') {
await expect(
sharedPage.getByRole('link', { name: translations.sidebar.bulkImport }),
).toBeVisible();
await sharedPage
.getByRole('link', { name: translations.sidebar.bulkImport })
.click();
} else {
await expect(
sharedPage.getByRole('link', { name: 'Bulk import' }),
).toBeVisible();
await sharedPage.getByRole('link', { name: 'Bulk import' }).click();
}
const bulkImportNavLabel = getBulkImportNavLabel(currentLocale);
await expect(
sharedPage.getByRole('link', { name: bulkImportNavLabel }),
).toBeVisible();
await sharedPage.getByRole('link', { name: bulkImportNavLabel }).click();
});

test.afterAll(async () => {
Expand Down
115 changes: 115 additions & 0 deletions workspaces/bulk-import/e2e-tests/permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright Red Hat, Inc.
*
* 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 { expect, test, type TestInfo } from '@playwright/test';

import {
mockBulkImportRepositoriesResponse,
mockBulkImportSCMHostsResponse,
mockRepositoriesData,
mockSCMHostsData,
} from './utils/apiUtils';
import {
installMockBulkImportPermission,
loginAsGuest,
} from './utils/permissionUtils';
import { getBulkImportNavLabel, getTranslations } from './utils/translations';

const isLegacy = process.env.APP_MODE === 'legacy';

function getLocaleContext(testInfo: TestInfo) {
const locale = testInfo.project.name;
const translations = getTranslations(locale);
const bulkImportNavLabel = getBulkImportNavLabel(locale);

return { locale, translations, bulkImportNavLabel };
}

test.describe('Bulk Import permissions', () => {
test('shows bulk import in the sidebar when bulk.import is allowed', async ({
page,
}, testInfo) => {
const { locale, bulkImportNavLabel } = getLocaleContext(testInfo);

await installMockBulkImportPermission(page, 'ALLOW');
await loginAsGuest(page, locale);

await expect(
page.getByRole('link', { name: bulkImportNavLabel }),
).toBeVisible();
});

test('hides bulk import from the sidebar when bulk.import is denied', async ({
page,
}, testInfo) => {
const { locale, bulkImportNavLabel } = getLocaleContext(testInfo);

await installMockBulkImportPermission(page, 'DENY');
await loginAsGuest(page, locale);

await expect(
page.getByRole('link', { name: bulkImportNavLabel }),
).not.toBeVisible();
});

test('opens the bulk import page when bulk.import is allowed', async ({
page,
}, testInfo) => {
const { locale, translations, bulkImportNavLabel } =
getLocaleContext(testInfo);

await installMockBulkImportPermission(page, 'ALLOW');
await mockBulkImportSCMHostsResponse(page, mockSCMHostsData);
await mockBulkImportRepositoriesResponse(page, mockRepositoriesData);
await loginAsGuest(page, locale);

await page.getByRole('link', { name: bulkImportNavLabel }).click();

await expect(
page.getByText(translations.addRepositories.approvalTool.title),
).toBeVisible();
});

test('denies direct navigation to bulk import when bulk.import is denied', async ({
page,
}, testInfo) => {
const { locale, translations, bulkImportNavLabel } =
getLocaleContext(testInfo);

await installMockBulkImportPermission(page, 'DENY');
await loginAsGuest(page, locale);

// Fresh navigation resets NFS if-predicate session state (per test page).
await page.goto('/bulk-import');

if (isLegacy) {
await expect(page.getByTestId('no-permission-alert')).toBeVisible({
timeout: 30000,
});
await expect(
page.getByText(translations.permissions.addRepositoriesMessage),
).toBeVisible();
return;
}

await expect(
page.getByText(translations.addRepositories.approvalTool.title),
).not.toBeVisible();
await expect(
page.getByRole('link', { name: bulkImportNavLabel }),
).not.toBeVisible();
});
});
5 changes: 3 additions & 2 deletions workspaces/bulk-import/e2e-tests/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export async function switchToLocale(
await page.getByRole('button', { name: 'English' }).click();
await page.getByRole('option', { name: displayName }).click();
await page.goto('/');
// Wait for page to settle after locale switch and reload
await page.waitForLoadState('networkidle');
await expect(page.getByText('All Components')).toBeVisible({
timeout: 30000,
});
}
}

Expand Down
96 changes: 96 additions & 0 deletions workspaces/bulk-import/e2e-tests/utils/permissionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright Red Hat, Inc.
*
* 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 { expect, Page } from '@playwright/test';

import { switchToLocale } from './helpers';

export const BULK_IMPORT_PERMISSION_NAME = 'bulk.import';

type AuthorizeItem = {
id: string;
permission?: {
name?: string;
};
};

type AuthorizeRequestBody = {
items?: AuthorizeItem[];
};

/**
* Mocks /api/permission/authorize for bulk.import while leaving other
* permission checks to the real backend.
*
* bulk.import is user-scoped (not per catalog entity), so we control allow/deny
* here instead of using multiple catalog entities like entity-scoped plugins.
*/
export async function installMockBulkImportPermission(
page: Page,
result: 'ALLOW' | 'DENY',
) {
await page.route('**/api/permission/authorize', async route => {
const request = route.request();
if (request.method() !== 'POST') {
await route.continue();
return;
}

let body: AuthorizeRequestBody;
try {
body = request.postDataJSON() as AuthorizeRequestBody;
} catch {
await route.continue();
return;
}

const items = body.items ?? [];
const affectsBulkImport = items.some(
item => item.permission?.name === BULK_IMPORT_PERMISSION_NAME,
);

if (!affectsBulkImport) {
await route.continue();
return;
}

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: items.map(item => ({
id: item.id,
result:
item.permission?.name === BULK_IMPORT_PERMISSION_NAME
? result
: 'ALLOW',
})),
}),
});
});
}

/** Sign in as guest, switch to the project locale, and wait for the catalog. */
export async function loginAsGuest(page: Page, locale: string) {
await page.goto('/');
const enterButton = page.getByRole('button', { name: 'Enter' });
await expect(enterButton).toBeVisible();
await enterButton.click();
await expect(page.getByText('All Components')).toBeVisible({
timeout: 30000,
});
await switchToLocale(page, locale);
}
11 changes: 11 additions & 0 deletions workspaces/bulk-import/e2e-tests/utils/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,14 @@ export function getSelectedRepositoriesHeading(
): string {
return `${translations.addRepositories.selectedLabel} ${translations.addRepositories.selectedRepositories} (${count})`;
}

/**
* Sidebar label for the bulk import page.
* Legacy uses translated nav text; NFS still uses a hardcoded English title (RHIDP-12094).
*/
export function getBulkImportNavLabel(locale: string): string {
if (process.env.APP_MODE === 'legacy') {
return getTranslations(locale).sidebar.bulkImport;
}
return 'Bulk import';
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ To set up the bulk import backend package for the backend:

#### Permission Framework Support

The Bulk Import Backend plugin has support for the permission framework. A basic example permission policy is shown below to disallow access to the bulk import API for all users except those in the `backstage-admins` group.
The Bulk Import Backend plugin has support for the permission framework. The plugin registers `bulk.import` as a **feature permission** (`BasicPermission`) from `@red-hat-developer-hub/backstage-plugin-bulk-import-common`.

A basic example permission policy is shown below to disallow access to the bulk import API for all users except those in the `backstage-admins` group.

1. Create a backend module for the permission policy, under a `packages/backend/src/plugins/permissions.ts` file:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export async function permissionCheck(
[
{
permission: bulkImportPermission,
resourceRef: bulkImportPermission.resourceType,
},
],
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { BasicPermission } from '@backstage/plugin-permission-common';

// @public
export const bulkImportPermission: ResourcePermission<'bulk-import'>;
export const bulkImportPermission: BasicPermission;
```
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
import { bulkImportPermission } from './permissions';

describe('bulkImportPermission', () => {
it('exports the stable permission name and resource type contract', () => {
it('exports the stable permission name as a feature permission', () => {
expect(bulkImportPermission.name).toBe('bulk.import');
expect(bulkImportPermission.resourceType).toBe('bulk-import');
expect(bulkImportPermission.attributes).toEqual({});
expect(bulkImportPermission).not.toHaveProperty('resourceType');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,4 @@ import { createPermission } from '@backstage/plugin-permission-common';
export const bulkImportPermission = createPermission({
name: 'bulk.import',
attributes: {},
resourceType: 'bulk-import',
});
Loading
Loading