add an actual mock implementation of permissions
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': minor
|
||||
---
|
||||
|
||||
Added `mockServices.permissions()` that can return actual results.
|
||||
@@ -52,6 +52,7 @@
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/plugin-events-node": "workspace:^",
|
||||
"@backstage/plugin-permission-common": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@keyv/memcache": "^2.0.1",
|
||||
"@keyv/redis": "^4.0.1",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api';
|
||||
import { AuditorService } from '@backstage/backend-plugin-api';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { Backend } from '@backstage/backend-app-api';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
@@ -299,14 +300,17 @@ export namespace mockServices {
|
||||
partialImpl?: Partial<LoggerService> | undefined,
|
||||
) => ServiceMock<LoggerService>;
|
||||
}
|
||||
export function permissions(options?: {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}): PermissionsService;
|
||||
// (undocumented)
|
||||
export namespace permissions {
|
||||
const // (undocumented)
|
||||
factory: () => ServiceFactory<PermissionsService, 'plugin', 'singleton'>;
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<PermissionsService> | undefined,
|
||||
) => ServiceMock<PermissionsService>;
|
||||
const factory: (options?: {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}) => ServiceFactory<PermissionsService, 'plugin', 'singleton'>;
|
||||
const mock: (
|
||||
partialImpl?: Partial<PermissionsService> | undefined,
|
||||
) => ServiceMock<PermissionsService>;
|
||||
}
|
||||
// (undocumented)
|
||||
export namespace permissionsRegistry {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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 { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { MockPermissionsService } from './MockPermissionsService';
|
||||
|
||||
describe('MockPermissionsService', () => {
|
||||
it('should return the correct result', async () => {
|
||||
const permission = {
|
||||
permission: {
|
||||
name: 'test',
|
||||
type: 'basic',
|
||||
attributes: {},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const defaultService = new MockPermissionsService();
|
||||
const allowService = new MockPermissionsService({
|
||||
result: AuthorizeResult.ALLOW,
|
||||
});
|
||||
const denyService = new MockPermissionsService({
|
||||
result: AuthorizeResult.DENY,
|
||||
});
|
||||
|
||||
await expect(defaultService.authorize([permission])).resolves.toEqual([
|
||||
{
|
||||
...permission,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
await expect(allowService.authorize([permission])).resolves.toEqual([
|
||||
{
|
||||
...permission,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
await expect(denyService.authorize([permission])).resolves.toEqual([
|
||||
{
|
||||
...permission,
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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 { PermissionsService } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
AuthorizePermissionRequest,
|
||||
AuthorizePermissionResponse,
|
||||
AuthorizeResult,
|
||||
QueryPermissionRequest,
|
||||
QueryPermissionResponse,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
|
||||
export class MockPermissionsService implements PermissionsService {
|
||||
readonly #result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
|
||||
constructor(options?: {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}) {
|
||||
this.#result = options?.result ?? AuthorizeResult.ALLOW;
|
||||
}
|
||||
|
||||
async authorize(
|
||||
requests: AuthorizePermissionRequest[],
|
||||
): Promise<AuthorizePermissionResponse[]> {
|
||||
return requests.map(request => ({
|
||||
...request,
|
||||
result: this.#result,
|
||||
}));
|
||||
}
|
||||
|
||||
async authorizeConditional(
|
||||
requests: QueryPermissionRequest[],
|
||||
): Promise<QueryPermissionResponse[]> {
|
||||
return requests.map(request => ({
|
||||
...request,
|
||||
result: this.#result,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
LoggerService,
|
||||
PermissionsService,
|
||||
RootConfigService,
|
||||
ServiceFactory,
|
||||
ServiceRef,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EventsService, eventsServiceRef } from '@backstage/plugin-events-node';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Knex } from 'knex';
|
||||
import { MockAuthService } from './MockAuthService';
|
||||
@@ -55,6 +57,7 @@ import { mockCredentials } from './mockCredentials';
|
||||
import { MockEventsService } from './MockEventsService';
|
||||
import { actionsServiceFactory } from '@backstage/backend-defaults/actions';
|
||||
import { actionsRegistryServiceFactory } from '@backstage/backend-defaults/actionsRegistry';
|
||||
import { MockPermissionsService } from './MockPermissionsService';
|
||||
|
||||
/** @internal */
|
||||
function createLoggerMock() {
|
||||
@@ -475,8 +478,37 @@ export namespace mockServices {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a functional mock implementation of the
|
||||
* {@link @backstage/backend-plugin-api#PermissionsService}.
|
||||
*/
|
||||
export function permissions(options?: {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}): PermissionsService {
|
||||
return new MockPermissionsService(options);
|
||||
}
|
||||
export namespace permissions {
|
||||
export const factory = () => permissionsServiceFactory;
|
||||
/**
|
||||
* Creates a mock factory for the
|
||||
* {@link @backstage/backend-plugin-api#coreServices.permissions}. Just
|
||||
* returns the given `result` if you supply one. Otherwise, it returns the
|
||||
* regular default permissions factory.
|
||||
*/
|
||||
export const factory = (options?: {
|
||||
result: AuthorizeResult.ALLOW | AuthorizeResult.DENY;
|
||||
}) =>
|
||||
options?.result
|
||||
? createServiceFactory({
|
||||
service: coreServices.permissions,
|
||||
deps: {},
|
||||
factory: () => new MockPermissionsService(options),
|
||||
})
|
||||
: permissionsServiceFactory;
|
||||
/**
|
||||
* Creates a mock of the
|
||||
* {@link @backstage/backend-plugin-api#coreServices.permissions},
|
||||
* optionally with some given method implementations.
|
||||
*/
|
||||
export const mock = simpleMock(coreServices.permissions, () => ({
|
||||
authorize: jest.fn(),
|
||||
authorizeConditional: jest.fn(),
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('createRouter readonly disabled', () => {
|
||||
let app: express.Express | Server;
|
||||
let refreshService: RefreshService;
|
||||
let locationAnalyzer: jest.Mocked<LocationAnalyzer>;
|
||||
const permissionsService = mockServices.permissions.mock();
|
||||
const permissionsService = mockServices.permissions();
|
||||
|
||||
beforeEach(async () => {
|
||||
entitiesCatalog = {
|
||||
@@ -799,12 +799,6 @@ describe('createRouter readonly disabled', () => {
|
||||
metadata: { name: 'n' },
|
||||
};
|
||||
|
||||
permissionsService.authorize.mockResolvedValueOnce([
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
state: {},
|
||||
@@ -845,12 +839,6 @@ describe('createRouter readonly disabled', () => {
|
||||
metadata: { name: 'invalid*name' },
|
||||
};
|
||||
|
||||
permissionsService.authorize.mockResolvedValueOnce([
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
|
||||
orchestrator.process.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
errors: [new Error('Invalid entity name')],
|
||||
@@ -888,12 +876,6 @@ describe('createRouter readonly disabled', () => {
|
||||
metadata: { name: 'n' },
|
||||
};
|
||||
|
||||
permissionsService.authorize.mockResolvedValueOnce([
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/validate-entity')
|
||||
.send({ entity, location: null });
|
||||
@@ -940,7 +922,7 @@ describe('createRouter readonly and raw json enabled', () => {
|
||||
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
|
||||
let app: express.Express;
|
||||
let locationService: jest.Mocked<LocationService>;
|
||||
const permissionsService = mockServices.permissions.mock();
|
||||
const permissionsService = mockServices.permissions();
|
||||
|
||||
beforeAll(async () => {
|
||||
entitiesCatalog = {
|
||||
@@ -1144,7 +1126,7 @@ describe('NextRouter permissioning', () => {
|
||||
let locationService: jest.Mocked<LocationService>;
|
||||
let app: express.Express;
|
||||
let refreshService: RefreshService;
|
||||
const permissionsService = mockServices.permissions.mock();
|
||||
const permissionsService = mockServices.permissions();
|
||||
|
||||
const fakeRule = createPermissionRule({
|
||||
name: 'FAKE_RULE',
|
||||
|
||||
@@ -18,30 +18,18 @@ import request from 'supertest';
|
||||
import {
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
type ServiceMock,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { kubernetesObjectsProviderExtensionPoint } from '@backstage/plugin-kubernetes-node';
|
||||
import {
|
||||
createBackendModule,
|
||||
type PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
describe('resourcesRoutes', () => {
|
||||
let app: ExtendedHttpServer;
|
||||
const permissionsMock: ServiceMock<PermissionsService> =
|
||||
mockServices.permissions.mock({
|
||||
authorize: jest.fn(),
|
||||
authorizeConditional: jest.fn(),
|
||||
});
|
||||
|
||||
const startPermissionDeniedTestServer = async () => {
|
||||
permissionsMock.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
@@ -52,7 +40,7 @@ describe('resourcesRoutes', () => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
permissionsMock.factory,
|
||||
mockServices.permissions.factory({ result: AuthorizeResult.DENY }),
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
} from './KubernetesProxy';
|
||||
import { setupServer } from 'msw/node';
|
||||
import {
|
||||
ServiceMock,
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
registerMswTestHooks,
|
||||
@@ -43,10 +42,7 @@ import {
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
PermissionsService,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
AuthMetadata,
|
||||
KubernetesObjectsProvider,
|
||||
@@ -64,11 +60,6 @@ describe('API integration tests', () => {
|
||||
const happyK8SResult = {
|
||||
items: [{ clusterOne: { pods: [{ metadata: { name: 'pod1' } }] } }],
|
||||
};
|
||||
const permissionsMock: ServiceMock<PermissionsService> =
|
||||
mockServices.permissions.mock({
|
||||
authorize: jest.fn(),
|
||||
authorizeConditional: jest.fn(),
|
||||
});
|
||||
const minimalValidConfigService = mockServices.rootConfig.factory({
|
||||
data: {
|
||||
kubernetes: {
|
||||
@@ -93,13 +84,10 @@ describe('API integration tests', () => {
|
||||
},
|
||||
});
|
||||
const startPermissionDeniedTestServer = async () => {
|
||||
permissionsMock.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.DENY },
|
||||
]);
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
minimalValidConfigService,
|
||||
permissionsMock.factory,
|
||||
mockServices.permissions.factory({ result: AuthorizeResult.DENY }),
|
||||
import('@backstage/plugin-kubernetes-backend'),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
registerMswTestHooks,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
|
||||
KubernetesRequestAuth,
|
||||
@@ -78,7 +77,7 @@ describe('KubernetesProxy', () => {
|
||||
>(),
|
||||
};
|
||||
|
||||
const permissionApi = mockServices.permissions.mock();
|
||||
const permissionApi = mockServices.permissions();
|
||||
const mockDisocveryApi = mockServices.discovery.mock();
|
||||
|
||||
registerMswTestHooks(worker);
|
||||
@@ -156,9 +155,6 @@ describe('KubernetesProxy', () => {
|
||||
authStrategy,
|
||||
discovery: mockDisocveryApi,
|
||||
});
|
||||
permissionApi.authorize.mockResolvedValue([
|
||||
{ result: AuthorizeResult.ALLOW },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a ERROR_NOT_FOUND if no clusters are found', async () => {
|
||||
|
||||
@@ -195,22 +195,7 @@ const createTestRouter = async (
|
||||
jest.spyOn(taskBroker, 'event$');
|
||||
|
||||
const catalog = catalogServiceMock.mock();
|
||||
const permissions = mockServices.permissions.mock();
|
||||
|
||||
permissions.authorizeConditional.mockImplementation(async p =>
|
||||
p.map(innerP => ({
|
||||
...innerP,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
})),
|
||||
);
|
||||
|
||||
permissions.authorize.mockImplementation(async p =>
|
||||
p.map(innerP => ({
|
||||
...innerP,
|
||||
result: AuthorizeResult.ALLOW,
|
||||
})),
|
||||
);
|
||||
|
||||
const permissions = mockServices.permissions();
|
||||
const auth = mockServices.auth();
|
||||
const httpAuth = mockServices.httpAuth();
|
||||
const events = mockServices.events();
|
||||
@@ -517,14 +502,16 @@ describe('scaffolder router', () => {
|
||||
|
||||
it('filters parameters that the user is not authorized to see', async () => {
|
||||
const { router, permissions } = await createTestRouter();
|
||||
permissions.authorizeConditional.mockImplementationOnce(async () => [
|
||||
{
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
jest
|
||||
.spyOn(permissions, 'authorizeConditional')
|
||||
.mockImplementationOnce(async () => [
|
||||
{
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(router)
|
||||
.get(
|
||||
@@ -541,21 +528,23 @@ describe('scaffolder router', () => {
|
||||
|
||||
it('filters parameters that the user is not authorized to see in case of conditional decision', async () => {
|
||||
const { permissions, router } = await createTestRouter();
|
||||
permissions.authorizeConditional.mockImplementation(async () => [
|
||||
{
|
||||
conditions: {
|
||||
jest
|
||||
.spyOn(permissions, 'authorizeConditional')
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
conditions: {
|
||||
resourceType: 'scaffolder-template',
|
||||
rule: 'HAS_TAG',
|
||||
params: { tag: 'parameters-tag' },
|
||||
},
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: 'scaffolder-template',
|
||||
rule: 'HAS_TAG',
|
||||
params: { tag: 'parameters-tag' },
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
},
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: 'scaffolder-template',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
},
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(router)
|
||||
.get(
|
||||
@@ -721,14 +710,16 @@ describe('scaffolder router', () => {
|
||||
|
||||
it('filters steps that the user is not authorized to see', async () => {
|
||||
const { router, permissions, taskBroker } = await createTestRouter();
|
||||
permissions.authorizeConditional.mockImplementation(async () => [
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
{
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
]);
|
||||
jest
|
||||
.spyOn(permissions, 'authorizeConditional')
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
{
|
||||
result: AuthorizeResult.DENY,
|
||||
},
|
||||
]);
|
||||
|
||||
const broker = taskBroker.dispatch as jest.Mocked<TaskBroker>['dispatch'];
|
||||
const mockTemplate = generateMockTemplate();
|
||||
@@ -786,21 +777,23 @@ describe('scaffolder router', () => {
|
||||
|
||||
it('filters steps that the user is not authorized to see in case of conditional decision', async () => {
|
||||
const { permissions, router, taskBroker } = await createTestRouter();
|
||||
permissions.authorizeConditional.mockImplementation(async () => [
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
{
|
||||
conditions: {
|
||||
resourceType: 'scaffolder-template',
|
||||
rule: 'HAS_TAG',
|
||||
params: { tag: 'steps-tag' },
|
||||
jest
|
||||
.spyOn(permissions, 'authorizeConditional')
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
result: AuthorizeResult.ALLOW,
|
||||
},
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: 'scaffolder-template',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
},
|
||||
]);
|
||||
{
|
||||
conditions: {
|
||||
resourceType: 'scaffolder-template',
|
||||
rule: 'HAS_TAG',
|
||||
params: { tag: 'steps-tag' },
|
||||
},
|
||||
pluginId: 'scaffolder',
|
||||
resourceType: 'scaffolder-template',
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
},
|
||||
]);
|
||||
|
||||
const broker = taskBroker.dispatch as jest.Mocked<TaskBroker>['dispatch'];
|
||||
const mockTemplate = generateMockTemplate();
|
||||
|
||||
@@ -3776,6 +3776,7 @@ __metadata:
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/plugin-events-node": "workspace:^"
|
||||
"@backstage/plugin-permission-common": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@keyv/memcache": "npm:^2.0.1"
|
||||
"@keyv/redis": "npm:^4.0.1"
|
||||
|
||||
Reference in New Issue
Block a user