permissions: add a discriminator type to Permission

Signed-off-by: Mike Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
Mike Lewis
2022-03-04 14:51:53 +00:00
committed by Joe Porpeglia
parent ada4446733
commit 9cbb270aef
12 changed files with 181 additions and 63 deletions
+7
View File
@@ -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 -1
View File
@@ -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({
+16 -7
View File
@@ -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' },
},
}),
},
};