diff --git a/plugins/permission-common/src/PermissionClient.test.ts b/plugins/permission-common/src/PermissionClient.test.ts index 65a7c7fbbe..ec038e7f16 100644 --- a/plugins/permission-common/src/PermissionClient.test.ts +++ b/plugins/permission-common/src/PermissionClient.test.ts @@ -22,6 +22,7 @@ import { EvaluatePermissionRequest, AuthorizeResult, IdentifiedPermissionMessage, + ConditionalPolicyDecision, } from './types/api'; import { DiscoveryApi } from './types/discovery'; import { createPermission } from './permissions'; @@ -140,7 +141,7 @@ describe('PermissionClient', () => { ); await expect( client.authorize([mockAuthorizeQuery], { token }), - ).rejects.toThrowError(/Unexpected authorization response/i); + ).rejects.toThrowError(/items in response do not match request/i); }); it('should reject invalid responses', async () => { @@ -209,4 +210,189 @@ describe('PermissionClient', () => { expect(mockAuthorizeHandler).not.toBeCalled(); }); }); + + describe('query', () => { + const mockResourceAuthorizeQuery = { + permission: mockPermission, + }; + + const mockPolicyDecisionHandler = jest.fn( + (req, res, { json }: RestContext) => { + const responses = req.body.items.map( + (a: IdentifiedPermissionMessage) => ({ + id: a.id, + pluginId: 'test-plugin', + resourceType: 'test-resource', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'FOO', + params: ['bar'], + }, + }), + ); + + return res(json({ items: responses })); + }, + ); + + beforeEach(() => { + server.use( + rest.post(`${mockBaseUrl}/authorize`, mockPolicyDecisionHandler), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch entities from correct endpoint', async () => { + await client.query([mockResourceAuthorizeQuery]); + expect(mockPolicyDecisionHandler).toHaveBeenCalled(); + }); + + it('should include a request body', async () => { + await client.query([mockResourceAuthorizeQuery]); + + const request = mockPolicyDecisionHandler.mock.calls[0][0]; + + expect(request.body).toEqual({ + items: [ + expect.objectContaining({ + permission: mockPermission, + }), + ], + }); + }); + + it('should return the response from the fetch request', async () => { + const response = await client.query([mockResourceAuthorizeQuery]); + expect(response[0]).toEqual( + expect.objectContaining({ + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'FOO', + params: ['bar'], + }, + }), + ); + }); + + it('should not include authorization headers if no token is supplied', async () => { + await client.query([mockResourceAuthorizeQuery]); + + const request = mockPolicyDecisionHandler.mock.calls[0][0]; + expect(request.headers.has('authorization')).toEqual(false); + }); + + it('should include correctly-constructed authorization header if token is supplied', async () => { + await client.query([mockResourceAuthorizeQuery], { + token, + }); + + const request = mockPolicyDecisionHandler.mock.calls[0][0]; + expect(request.headers.get('authorization')).toEqual('Bearer fake-token'); + }); + + it('should forward response errors', async () => { + mockPolicyDecisionHandler.mockImplementationOnce( + (_req, res, { status }: RestContext) => { + return res(status(401)); + }, + ); + await expect( + client.query([mockResourceAuthorizeQuery], { + token, + }), + ).rejects.toThrowError(/request failed with 401/i); + }); + + it('should reject responses with missing ids', async () => { + mockPolicyDecisionHandler.mockImplementationOnce( + (_req, res, { json }: RestContext) => { + return res( + json({ + items: [{ id: 'wrong-id', result: AuthorizeResult.ALLOW }], + }), + ); + }, + ); + await expect( + client.query([mockResourceAuthorizeQuery], { + token, + }), + ).rejects.toThrowError(/items in response do not match request/i); + }); + + it('should reject invalid responses', async () => { + mockPolicyDecisionHandler.mockImplementationOnce( + (req, res, { json }: RestContext) => { + const responses = req.body.items.map( + (a: IdentifiedPermissionMessage) => ({ + id: a.id, + outcome: AuthorizeResult.ALLOW, + }), + ); + + return res(json({ items: responses })); + }, + ); + await expect( + client.query([mockResourceAuthorizeQuery], { + token, + }), + ).rejects.toThrowError(/invalid input/i); + }); + + it('should allow all when permission.enabled is false', async () => { + mockPolicyDecisionHandler.mockImplementationOnce( + (req, res, { json }: RestContext) => { + const responses = req.body.map( + (a: IdentifiedPermissionMessage) => ({ + id: a.id, + result: AuthorizeResult.DENY, + }), + ); + + return res(json({ items: responses })); + }, + ); + const disabled = new PermissionClient({ + discovery, + config: new ConfigReader({ permission: { enabled: false } }), + }); + const response = await disabled.query([mockResourceAuthorizeQuery], { + token, + }); + expect(response[0]).toEqual( + expect.objectContaining({ result: AuthorizeResult.ALLOW }), + ); + expect(mockPolicyDecisionHandler).not.toBeCalled(); + }); + + it('should allow all when permission.enabled is not configured', async () => { + mockPolicyDecisionHandler.mockImplementationOnce( + (req, res, { json }: RestContext) => { + const responses = req.body.map( + (a: IdentifiedPermissionMessage) => ({ + id: a.id, + outcome: AuthorizeResult.DENY, + }), + ); + + return res(json(responses)); + }, + ); + const disabled = new PermissionClient({ + discovery, + config: new ConfigReader({}), + }); + const response = await disabled.query([mockResourceAuthorizeQuery], { + token, + }); + expect(response[0]).toEqual( + expect.objectContaining({ result: AuthorizeResult.ALLOW }), + ); + expect(mockPolicyDecisionHandler).not.toBeCalled(); + }); + }); }); diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index 72f6b04567..9ca9bfe255 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -21,19 +21,20 @@ import * as uuid from 'uuid'; import { z } from 'zod'; import { AuthorizeResult, - EvaluatePermissionRequest, - EvaluatePermissionResponse, - IdentifiedPermissionMessage, + DefinitivePolicyDecision, + PermissionMessageBatch, PermissionCriteria, PermissionCondition, - EvaluatePermissionResponseBatch, - EvaluatePermissionRequestBatch, + PermissionEvaluator, + PolicyDecision, + QueryPermissionRequest, + AuthorizePermissionRequest, + EvaluatorRequestOptions, + AuthorizePermissionResponse, + QueryPermissionResponse, } from './types/api'; import { DiscoveryApi } from './types/discovery'; -import { - PermissionAuthorizer, - AuthorizeRequestOptions, -} from './types/permission'; +import { AuthorizeRequestOptions } from './types/permission'; const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria @@ -58,30 +59,56 @@ const permissionCriteriaSchema: z.ZodSchema< .or(z.object({ not: permissionCriteriaSchema }).strict()), ); -const responseSchema = z.object({ - items: z.array( - z - .object({ - id: z.string(), - result: z - .literal(AuthorizeResult.ALLOW) - .or(z.literal(AuthorizeResult.DENY)), - }) - .or( - z.object({ - id: z.string(), - result: z.literal(AuthorizeResult.CONDITIONAL), - conditions: permissionCriteriaSchema, - }), +const authorizeDecisionSchema: z.ZodSchema = z.object( + { + result: z + .literal(AuthorizeResult.ALLOW) + .or(z.literal(AuthorizeResult.DENY)), + }, +); + +const policyDecisionSchema: z.ZodSchema = z.union([ + z.object({ + result: z + .literal(AuthorizeResult.ALLOW) + .or(z.literal(AuthorizeResult.DENY)), + }), + z.object({ + result: z.literal(AuthorizeResult.CONDITIONAL), + pluginId: z.string(), + resourceType: z.string(), + conditions: permissionCriteriaSchema, + }), +]); + +const responseSchema = ( + itemSchema: z.ZodSchema, + ids: Set, +): z.ZodSchema> => + z.object({ + items: z + .array( + z.intersection( + z.object({ + id: z.string(), + }), + itemSchema, + ), + ) + .refine( + items => + items.length === ids.size && items.every(({ id }) => ids.has(id)), + { + message: 'Items in response do not match request', + }, ), - ), -}); + }); /** * An isomorphic client for requesting authorization for Backstage permissions. * @public */ -export class PermissionClient implements PermissionAuthorizer { +export class PermissionClient implements PermissionEvaluator { private readonly enabled: boolean; private readonly discovery: DiscoveryApi; @@ -92,35 +119,67 @@ export class PermissionClient implements PermissionAuthorizer { } /** - * Request authorization from the permission-backend for the given set of permissions. + * Request authorization from the permission-backend for the given set of + * permissions. * - * Authorization requests check that a given Backstage user can perform a protected operation, - * potentially for a specific resource (such as a catalog entity). The Backstage identity token - * should be included in the `options` if available. + * @remarks * - * Permissions can be imported from plugins exposing them, such as `catalogEntityReadPermission`. + * Checks that a given Backstage user can perform a protected operation. When + * authorization is for a {@link ResourcePermission}s, a resourceRef + * corresponding to the resource should always be supplied along with the + * permission. The Backstage identity token should be included in the + * `options` if available. + * + * Permissions can be imported from plugins exposing them, such as + * `catalogEntityReadPermission`. + * + * For each query, the response will be either ALLOW or DENY. * - * The response will be either ALLOW or DENY when either the permission has no resourceType, or a - * resourceRef is provided in the request. For permissions with a resourceType, CONDITIONAL may be - * returned if no resourceRef is provided in the request. Conditional responses are intended only - * for backends which have access to the data source for permissioned resources, so that filters - * can be applied when loading collections of resources. * @public */ async authorize( - queries: EvaluatePermissionRequest[], - options?: AuthorizeRequestOptions, - ): Promise { + requests: AuthorizePermissionRequest[], + options?: EvaluatorRequestOptions, + ): Promise { // TODO(permissions): it would be great to provide some kind of typing guarantee that // conditional responses will only ever be returned for requests containing a resourceType // but no resourceRef. That way clients who aren't prepared to handle filtering according // to conditions can be guaranteed that they won't unexpectedly get a CONDITIONAL response. + return this.makeRequest(requests, authorizeDecisionSchema, options); + } + + /** + * Fetch the conditional authorization decisions for the given set of + * {@link ResourcePermission}s in order to apply the conditions to an upstream + * data source. + * + * @remarks + * + * For each query, the response will be either ALLOW, DENY, or CONDITIONAL. + * Conditional responses are intended only for backends which have access to + * the data source for permissioned resources, so that filters can be applied + * when loading collections of resources. + * + * @public + */ + async query( + queries: QueryPermissionRequest[], + options?: EvaluatorRequestOptions, + ): Promise { + return this.makeRequest(queries, policyDecisionSchema, options); + } + + private async makeRequest( + queries: TQuery[], + itemSchema: z.ZodSchema, + options?: AuthorizeRequestOptions, + ) { if (!this.enabled) { - return queries.map(_ => ({ result: AuthorizeResult.ALLOW })); + return queries.map(_ => ({ result: AuthorizeResult.ALLOW as const })); } - const request: EvaluatePermissionRequestBatch = { + const request = { items: queries.map(query => ({ id: uuid.v4(), ...query, @@ -141,12 +200,16 @@ export class PermissionClient implements PermissionAuthorizer { } const responseBody = await response.json(); - this.assertValidResponse(request, responseBody); - const responsesById = responseBody.items.reduce((acc, r) => { + const parsedResponse = responseSchema( + itemSchema, + new Set(request.items.map(({ id }) => id)), + ).parse(responseBody); + + const responsesById = parsedResponse.items.reduce((acc, r) => { acc[r.id] = r; return acc; - }, {} as Record>); + }, {} as Record); return request.items.map(query => responsesById[query.id]); } @@ -154,20 +217,4 @@ export class PermissionClient implements PermissionAuthorizer { private getAuthorizationHeader(token?: string): Record { return token ? { Authorization: `Bearer ${token}` } : {}; } - - private assertValidResponse( - request: EvaluatePermissionRequestBatch, - json: any, - ): asserts json is EvaluatePermissionResponseBatch { - const authorizedResponses = responseSchema.parse(json); - const responseIds = authorizedResponses.items.map(r => r.id); - const hasAllRequestIds = request.items.every(r => - responseIds.includes(r.id), - ); - if (!hasAllRequestIds) { - throw new Error( - 'Unexpected authorization response from permission-backend', - ); - } - } } diff --git a/plugins/permission-common/src/types/api.ts b/plugins/permission-common/src/types/api.ts index 1bc9af5ad8..c1996fc462 100644 --- a/plugins/permission-common/src/types/api.ts +++ b/plugins/permission-common/src/types/api.ts @@ -91,6 +91,20 @@ export type PolicyDecision = | DefinitivePolicyDecision | ConditionalPolicyDecision; +/** + * A query to be evaluated by the {@link PermissionPolicy}. + * + * @remarks + * + * Unlike other parts of the permission API, the policy does not accept a resource ref. This keeps + * the policy decoupled from the resource loading and condition applying logic. + * + * @public + */ +export type PolicyQuery = { + permission: Permission; +}; + /** * A condition returned with a CONDITIONAL authorization response. * diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts index f244a70b1a..dd644ff345 100644 --- a/plugins/permission-common/src/types/index.ts +++ b/plugins/permission-common/src/types/index.ts @@ -36,6 +36,7 @@ export type { AllOfCriteria, AnyOfCriteria, NotCriteria, + PolicyQuery, } from './api'; export type { DiscoveryApi } from './discovery'; export type { diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index f5711ed4a0..2e3e5d988f 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -17,9 +17,10 @@ import { ServerPermissionClient } from './ServerPermissionClient'; import { IdentifiedPermissionMessage, - EvaluatePermissionRequest, AuthorizeResult, createPermission, + DefinitivePolicyDecision, + ConditionalPolicyDecision, } from '@backstage/plugin-permission-common'; import { ConfigReader } from '@backstage/config'; import { @@ -31,16 +32,7 @@ import { setupServer } from 'msw/node'; import { RestContext, rest } from 'msw'; const server = setupServer(); -const mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { - const responses = req.body.items.map( - (r: IdentifiedPermissionMessage) => ({ - id: r.id, - result: AuthorizeResult.ALLOW, - }), - ); - return res(json({ items: responses })); -}); const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discovery: PluginEndpointDiscovery = { async getBaseUrl() { @@ -50,10 +42,17 @@ const discovery: PluginEndpointDiscovery = { return mockBaseUrl; }, }; -const testPermission = createPermission({ +const testBasicPermission = createPermission({ name: 'test.permission', attributes: {}, }); + +const testResourcePermission = createPermission({ + name: 'test.permission-2', + attributes: {}, + resourceType: 'resource-type', +}); + const config = new ConfigReader({ permission: { enabled: true }, backend: { auth: { keys: [{ secret: 'a-secret-key' }] } }, @@ -63,49 +62,6 @@ const logger = getVoidLogger(); describe('ServerPermissionClient', () => { beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close()); - beforeEach(() => { - server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); - }); - afterEach(() => server.resetHandlers()); - - it('should bypass authorization if permissions are disabled', async () => { - const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { - discovery, - tokenManager: ServerTokenManager.noop(), - }); - - await client.authorize([{ permission: testPermission }]); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass authorization if permissions are enabled and request has valid server token', async () => { - const tokenManager = ServerTokenManager.fromConfig(config, { logger }); - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - - await client.authorize([{ permission: testPermission }], { - token: (await tokenManager.getToken()).token, - }); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should authorize normally if permissions are enabled and request does not have valid server token', async () => { - const tokenManager = ServerTokenManager.fromConfig(config, { logger }); - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - - await client.authorize([{ permission: testPermission }], { - token: 'a-user-token', - }); - - expect(mockAuthorizeHandler).toHaveBeenCalled(); - }); it('should error if permissions are enabled but a no-op token manager is configured', async () => { expect(() => @@ -117,4 +73,126 @@ describe('ServerPermissionClient', () => { 'Backend-to-backend authentication must be configured before enabling permissions. Read more here https://backstage.io/docs/tutorials/backend-to-backend-auth', ); }); + + describe('authorize', () => { + let mockAuthorizeHandler: jest.Mock; + + beforeEach(() => { + mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { + const responses = req.body.items.map( + (r: IdentifiedPermissionMessage) => ({ + id: r.id, + result: AuthorizeResult.ALLOW, + }), + ); + + return res(json({ items: responses })); + }); + + server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); + }); + afterEach(() => server.resetHandlers()); + + it('should bypass the permission backend if permissions are disabled', async () => { + const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { + discovery, + tokenManager: ServerTokenManager.noop(), + }); + + await client.authorize([ + { + permission: testBasicPermission, + }, + ]); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { + const tokenManager = ServerTokenManager.fromConfig(config, { logger }); + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + + await client.authorize([{ permission: testBasicPermission }], { + token: (await tokenManager.getToken()).token, + }); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { + const tokenManager = ServerTokenManager.fromConfig(config, { logger }); + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + + await client.authorize([{ permission: testBasicPermission }], { + token: 'a-user-token', + }); + + expect(mockAuthorizeHandler).toHaveBeenCalled(); + }); + }); + + describe('query', () => { + let mockAuthorizeHandler: jest.Mock; + + beforeEach(() => { + mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { + const responses = req.body.items.map( + (r: IdentifiedPermissionMessage) => ({ + id: r.id, + result: AuthorizeResult.ALLOW, + }), + ); + + return res(json({ items: responses })); + }); + + server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); + }); + afterEach(() => server.resetHandlers()); + + it('should bypass the permission backend if permissions are disabled', async () => { + const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { + discovery, + tokenManager: ServerTokenManager.noop(), + }); + + await client.query([{ permission: testResourcePermission }]); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { + const tokenManager = ServerTokenManager.fromConfig(config, { logger }); + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + + await client.query([{ permission: testResourcePermission }], { + token: (await tokenManager.getToken()).token, + }); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { + const tokenManager = ServerTokenManager.fromConfig(config, { logger }); + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); + + await client.query([{ permission: testResourcePermission }], { + token: 'a-user-token', + }); + + expect(mockAuthorizeHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/plugins/permission-node/src/ServerPermissionClient.ts b/plugins/permission-node/src/ServerPermissionClient.ts index 56381dff17..2ea11e9e95 100644 --- a/plugins/permission-node/src/ServerPermissionClient.ts +++ b/plugins/permission-node/src/ServerPermissionClient.ts @@ -20,12 +20,14 @@ import { } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { - EvaluatePermissionRequest, - AuthorizeRequestOptions, - EvaluatePermissionResponse, AuthorizeResult, PermissionClient, - PermissionAuthorizer, + PermissionEvaluator, + AuthorizePermissionRequest, + EvaluatorRequestOptions, + AuthorizePermissionResponse, + PolicyDecision, + QueryPermissionRequest, } from '@backstage/plugin-permission-common'; /** @@ -34,7 +36,7 @@ import { * backend-to-backend requests. * @public */ -export class ServerPermissionClient implements PermissionAuthorizer { +export class ServerPermissionClient implements PermissionEvaluator { private readonly permissionClient: PermissionClient; private readonly tokenManager: TokenManager; private readonly permissionEnabled: boolean; @@ -76,22 +78,22 @@ export class ServerPermissionClient implements PermissionAuthorizer { this.tokenManager = options.tokenManager; this.permissionEnabled = options.permissionEnabled; } + async query( + queries: QueryPermissionRequest[], + options?: EvaluatorRequestOptions, + ): Promise { + return (await this.isEnabled(options?.token)) + ? this.permissionClient.query(queries, options) + : queries.map(_ => ({ result: AuthorizeResult.ALLOW })); + } async authorize( - requests: EvaluatePermissionRequest[], - options?: AuthorizeRequestOptions, - ): Promise { - // Check if permissions are enabled before validating the server token. That - // way when permissions are disabled, the noop token manager can be used - // without fouling up the logic inside the ServerPermissionClient, because - // the code path won't be reached. - if ( - !this.permissionEnabled || - (await this.isValidServerToken(options?.token)) - ) { - return requests.map(_ => ({ result: AuthorizeResult.ALLOW })); - } - return this.permissionClient.authorize(requests, options); + requests: AuthorizePermissionRequest[], + options?: EvaluatorRequestOptions, + ): Promise { + return (await this.isEnabled(options?.token)) + ? this.permissionClient.authorize(requests, options) + : requests.map(_ => ({ result: AuthorizeResult.ALLOW })); } private async isValidServerToken( @@ -105,4 +107,12 @@ export class ServerPermissionClient implements PermissionAuthorizer { .then(() => true) .catch(() => false); } + + private async isEnabled(token?: string) { + // Check if permissions are enabled before validating the server token. That + // way when permissions are disabled, the noop token manager can be used + // without fouling up the logic inside the ServerPermissionClient, because + // the code path won't be reached. + return this.permissionEnabled && !(await this.isValidServerToken(token)); + } } diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts index f151cfb4fb..0b7e03cc7c 100644 --- a/plugins/permission-node/src/policy/index.ts +++ b/plugins/permission-node/src/policy/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { PermissionPolicy, PolicyQuery } from './types'; +export type { PermissionPolicy } from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 0f469cfbfe..992ad3a1cf 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -15,25 +15,11 @@ */ import { - Permission, PolicyDecision, + PolicyQuery, } from '@backstage/plugin-permission-common'; import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; -/** - * A query to be evaluated by the {@link PermissionPolicy}. - * - * @remarks - * - * Unlike other parts of the permission API, the policy does not accept a resource ref. This keeps - * the policy decoupled from the resource loading and condition applying logic. - * - * @public - */ -export type PolicyQuery = { - permission: Permission; -}; - /** * A policy to evaluate authorization requests for any permissioned action performed in Backstage. *