permissions: add a discriminator type to Permission
Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
committed by
Joe Porpeglia
parent
ada4446733
commit
9cbb270aef
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-permission-react': patch
|
||||
'@backstage/plugin-search-backend': patch
|
||||
'@backstage/test-utils': patch
|
||||
---
|
||||
|
||||
Use `createPermission` helper to create valid `Permission` objects in test suites.
|
||||
@@ -2,4 +2,5 @@
|
||||
'@backstage/plugin-permission-backend': patch
|
||||
---
|
||||
|
||||
Add more specific check for policies which return conditional decisions for non-resource permissions.
|
||||
- Add more specific check for policies which return conditional decisions for non-resource permissions.
|
||||
- Refine permission validation in authorize endpoint to differentiate between `BasicPermission` and `ResourcePermission` instances.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import {
|
||||
AuthorizeResult,
|
||||
Permission,
|
||||
createPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { MockPermissionApi } from './MockPermissionApi';
|
||||
|
||||
@@ -25,7 +25,9 @@ describe('MockPermissionApi', () => {
|
||||
const api = new MockPermissionApi();
|
||||
|
||||
await expect(
|
||||
api.authorize({ permission: { name: 'permission.1' } as Permission }),
|
||||
api.authorize({
|
||||
permission: createPermission({ name: 'permission.1', attributes: {} }),
|
||||
}),
|
||||
).resolves.toEqual({ result: AuthorizeResult.ALLOW });
|
||||
});
|
||||
|
||||
@@ -37,7 +39,9 @@ describe('MockPermissionApi', () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
api.authorize({ permission: { name: 'permission.2' } as Permission }),
|
||||
api.authorize({
|
||||
permission: createPermission({ name: 'permission.2', attributes: {} }),
|
||||
}),
|
||||
).resolves.toEqual({ result: AuthorizeResult.DENY });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,6 +110,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission1',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -117,6 +118,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission2',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -129,6 +131,7 @@ describe('createRouter', () => {
|
||||
expect(policy.handle).toHaveBeenCalledWith(
|
||||
{
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission1',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -138,6 +141,7 @@ describe('createRouter', () => {
|
||||
expect(policy.handle).toHaveBeenCalledWith(
|
||||
{
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission2',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -163,6 +167,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -174,6 +179,7 @@ describe('createRouter', () => {
|
||||
expect(policy.handle).toHaveBeenCalledWith(
|
||||
{
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -201,6 +207,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -258,6 +265,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -267,6 +275,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
@@ -276,6 +285,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -285,6 +295,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
@@ -384,6 +395,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -393,6 +405,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
@@ -402,6 +415,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -411,6 +425,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -420,6 +435,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '567',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.5',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
@@ -429,6 +445,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '678',
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission.6',
|
||||
attributes: {},
|
||||
},
|
||||
@@ -519,6 +536,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.1',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -528,6 +546,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '234',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.2',
|
||||
resourceType: 'test-resource-2',
|
||||
attributes: {},
|
||||
@@ -537,6 +556,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '345',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.3',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -546,6 +566,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '456',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission.4',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -630,6 +651,7 @@ describe('createRouter', () => {
|
||||
id: '123',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -639,6 +661,7 @@ describe('createRouter', () => {
|
||||
id: '234',
|
||||
resourceRef: 'test/resource',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
@@ -687,10 +710,37 @@ describe('createRouter', () => {
|
||||
undefined,
|
||||
'',
|
||||
{},
|
||||
[{ permission: { name: 'test.permission', attributes: {} } }],
|
||||
{ items: [{ permission: { name: 'test.permission', attributes: {} } }] },
|
||||
[
|
||||
{
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
items: [
|
||||
{
|
||||
permission: {
|
||||
type: 'basic',
|
||||
name: 'test.permission',
|
||||
attributes: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ items: [{ id: '123' }] },
|
||||
{ items: [{ id: '123', permission: { name: 'test.permission' } }] },
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: '123',
|
||||
permission: { name: 'test.permission', attributes: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ items: [{ id: '123', permission: { type: 'basic', attributes: {} } }] },
|
||||
{ items: [{ id: '123', permission: { type: 'basic' } }] },
|
||||
{
|
||||
items: [
|
||||
{ id: '123', permission: { attributes: { invalid: 'attribute' } } },
|
||||
@@ -724,6 +774,7 @@ describe('createRouter', () => {
|
||||
{
|
||||
id: '123',
|
||||
permission: {
|
||||
type: 'resource',
|
||||
name: 'test.permission',
|
||||
resourceType: 'test-resource-1',
|
||||
attributes: {},
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
isResourcePermission,
|
||||
PermissionAttributes,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
ApplyConditionsRequestEntry,
|
||||
@@ -47,23 +48,35 @@ import { memoize } from 'lodash';
|
||||
import DataLoader from 'dataloader';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const attributesSchema: z.ZodSchema<PermissionAttributes> = z.object({
|
||||
action: z
|
||||
.union([
|
||||
z.literal('create'),
|
||||
z.literal('read'),
|
||||
z.literal('update'),
|
||||
z.literal('delete'),
|
||||
])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const permissionSchema = z.union([
|
||||
z.object({
|
||||
type: z.literal('basic'),
|
||||
name: z.string(),
|
||||
attributes: attributesSchema,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('resource'),
|
||||
name: z.string(),
|
||||
attributes: attributesSchema,
|
||||
resourceType: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const querySchema: z.ZodSchema<Identified<AuthorizeQuery>> = z.object({
|
||||
id: z.string(),
|
||||
resourceRef: z.string().optional(),
|
||||
permission: z.object({
|
||||
name: z.string(),
|
||||
resourceType: z.string().optional(),
|
||||
attributes: z.object({
|
||||
action: z
|
||||
.union([
|
||||
z.literal('create'),
|
||||
z.literal('read'),
|
||||
z.literal('update'),
|
||||
z.literal('delete'),
|
||||
])
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
permission: permissionSchema,
|
||||
});
|
||||
|
||||
const requestSchema: z.ZodSchema<AuthorizeRequest> = z.object({
|
||||
|
||||
@@ -54,10 +54,7 @@ export enum AuthorizeResult {
|
||||
}
|
||||
|
||||
// @public
|
||||
export type BasicPermission = {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
};
|
||||
export type BasicPermission = PermissionBase<'basic', {}>;
|
||||
|
||||
// @public
|
||||
export function createPermission<TResourceType extends string>(input: {
|
||||
@@ -122,6 +119,14 @@ export interface PermissionAuthorizer {
|
||||
): Promise<AuthorizeDecision[]>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type PermissionBase<TType extends string, TFields extends object> = {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
} & {
|
||||
type: TType;
|
||||
} & TFields;
|
||||
|
||||
// @public
|
||||
export class PermissionClient implements PermissionAuthorizer {
|
||||
constructor(options: { discovery: DiscoveryApi; config: Config });
|
||||
@@ -145,7 +150,11 @@ export type PermissionCriteria<TQuery> =
|
||||
| TQuery;
|
||||
|
||||
// @public
|
||||
export type ResourcePermission<T extends string = string> = BasicPermission & {
|
||||
resourceType: T;
|
||||
};
|
||||
export type ResourcePermission<TResourceType extends string = string> =
|
||||
PermissionBase<
|
||||
'resource',
|
||||
{
|
||||
resourceType: TResourceType;
|
||||
}
|
||||
>;
|
||||
```
|
||||
|
||||
@@ -41,10 +41,27 @@ export function createPermission(input: {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
}): BasicPermission;
|
||||
export function createPermission(input: {
|
||||
export function createPermission({
|
||||
name,
|
||||
attributes,
|
||||
resourceType,
|
||||
}: {
|
||||
name: string;
|
||||
attributes: PermissionAttributes;
|
||||
resourceType?: string;
|
||||
}): Permission {
|
||||
return input;
|
||||
if (resourceType) {
|
||||
return {
|
||||
type: 'resource',
|
||||
name,
|
||||
attributes,
|
||||
resourceType,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'basic',
|
||||
name,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export type {
|
||||
PermissionAttributes,
|
||||
Permission,
|
||||
PermissionAuthorizer,
|
||||
PermissionBase,
|
||||
ResourcePermission,
|
||||
AuthorizeRequestOptions,
|
||||
} from './permission';
|
||||
|
||||
@@ -25,6 +25,23 @@ export type PermissionAttributes = {
|
||||
action?: 'create' | 'read' | 'update' | 'delete';
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic type for building {@link Permission} types.
|
||||
* @public
|
||||
*/
|
||||
export type PermissionBase<TType extends string, TFields extends object> = {
|
||||
/**
|
||||
* The name of the permission.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* {@link PermissionAttributes} which describe characteristics of the permission, to help
|
||||
* policy authors make consistent decisions for similar permissions without referring to them
|
||||
* all by name.
|
||||
*/
|
||||
attributes: PermissionAttributes;
|
||||
} & { type: TType } & TFields;
|
||||
|
||||
/**
|
||||
* A permission that can be checked through authorization.
|
||||
*
|
||||
@@ -44,31 +61,24 @@ export type Permission = BasicPermission | ResourcePermission;
|
||||
* A standard {@link Permission} with no additional capabilities or restrictions.
|
||||
* @public
|
||||
*/
|
||||
export type BasicPermission = {
|
||||
/**
|
||||
* The name of the permission.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* {@link PermissionAttributes} which describe characteristics of the permission, to help
|
||||
* policy authors make consistent decisions for similar permissions without referring to them
|
||||
* all by name.
|
||||
*/
|
||||
attributes: PermissionAttributes;
|
||||
};
|
||||
export type BasicPermission = PermissionBase<'basic', {}>;
|
||||
|
||||
/**
|
||||
* ResourcePermissions are {@link Permission}s that can be authorized based on
|
||||
* characteristics of a resource such a catalog entity.
|
||||
* @public
|
||||
*/
|
||||
export type ResourcePermission<T extends string = string> = BasicPermission & {
|
||||
/**
|
||||
* Denotes the type of the resource whose resourceRef should be passed when
|
||||
* authorizing.
|
||||
*/
|
||||
resourceType: T;
|
||||
};
|
||||
export type ResourcePermission<TResourceType extends string = string> =
|
||||
PermissionBase<
|
||||
'resource',
|
||||
{
|
||||
/**
|
||||
* Denotes the type of the resource whose resourceRef should be passed when
|
||||
* authorizing.
|
||||
*/
|
||||
resourceType: TResourceType;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* A client interacting with the permission backend can implement this authorizer interface.
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import { PermissionedRoute } from '.';
|
||||
import { usePermission } from '../hooks';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { createPermission } from '@backstage/plugin-permission-common';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
usePermission: jest.fn(),
|
||||
@@ -26,10 +27,10 @@ const mockUsePermission = usePermission as jest.MockedFunction<
|
||||
typeof usePermission
|
||||
>;
|
||||
|
||||
const permission = {
|
||||
const permission = createPermission({
|
||||
name: 'access.something',
|
||||
attributes: { action: 'read' as const },
|
||||
};
|
||||
});
|
||||
|
||||
describe('PermissionedRoute', () => {
|
||||
it('Does not render when loading', async () => {
|
||||
|
||||
@@ -17,15 +17,18 @@
|
||||
import React, { FC } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { usePermission } from './usePermission';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
createPermission,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { PermissionApi, permissionApiRef } from '../apis';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
const permission = {
|
||||
const permission = createPermission({
|
||||
name: 'access.something',
|
||||
attributes: { action: 'read' as const },
|
||||
};
|
||||
attributes: { action: 'read' },
|
||||
});
|
||||
|
||||
const TestComponent: FC = () => {
|
||||
const { loading, allowed, error } = usePermission(permission);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
AuthorizeDecision,
|
||||
AuthorizeResult,
|
||||
createPermission,
|
||||
PermissionAuthorizer,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
@@ -78,28 +79,28 @@ describe('AuthorizedSearchEngine', () => {
|
||||
|
||||
const defaultTypes: Record<string, DocumentTypeInfo> = {
|
||||
[typeUsers]: {
|
||||
visibilityPermission: {
|
||||
visibilityPermission: createPermission({
|
||||
name: 'search.users.read',
|
||||
attributes: { action: 'read' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
[typeTemplates]: {
|
||||
visibilityPermission: {
|
||||
visibilityPermission: createPermission({
|
||||
name: 'search.templates.read',
|
||||
attributes: { action: 'read' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
[typeServices]: {
|
||||
visibilityPermission: {
|
||||
visibilityPermission: createPermission({
|
||||
name: 'search.services.read',
|
||||
attributes: { action: 'read' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
[typeGroups]: {
|
||||
visibilityPermission: {
|
||||
visibilityPermission: createPermission({
|
||||
name: 'search.groups.read',
|
||||
attributes: { action: 'read' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user