permission-backend: accept resourceRef as array

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2025-04-29 10:46:10 +02:00
parent 669038aab8
commit 7853dac010
7 changed files with 47 additions and 157 deletions
@@ -154,7 +154,7 @@ describe('createRouter', () => {
});
});
it('calls the permission policy with batched resourceRefs', async () => {
it('calls the permission policy with batched resourceRef as an array', async () => {
policy.handle.mockResolvedValueOnce({
result: AuthorizeResult.CONDITIONAL,
pluginId: 'test-plugin',
@@ -181,7 +181,7 @@ describe('createRouter', () => {
attributes: {},
resourceType: 'test-resource-1',
},
resourceRefs: ['resource:1', 'resource:2'],
resourceRef: ['resource:1', 'resource:2'],
},
{
id: '234',
@@ -202,7 +202,7 @@ describe('createRouter', () => {
conditions: { params: ['abc'], rule: 'test-rule' },
id: '123',
pluginId: 'test-plugin',
resourceRefs: ['resource:1', 'resource:2'],
resourceRef: ['resource:1', 'resource:2'],
resourceType: 'test-resource-1',
result: 'CONDITIONAL',
},
@@ -611,7 +611,7 @@ describe('createRouter', () => {
});
});
it('leaves conditional results without resourceRefs unchanged', async () => {
it('leaves conditional results without resourceRef unchanged', async () => {
policy.handle
.mockResolvedValueOnce({
result: AuthorizeResult.CONDITIONAL,
@@ -881,79 +881,7 @@ describe('createRouter', () => {
items: [
{
id: '123',
// resource ref should be a string
resourceRef: ['resource:1'],
permission: {
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
{
items: [
{
id: '123',
resourceRef: ['resource:1'],
permission: {
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
{
items: [
{
id: '123',
resourceRefs: 'resource:1',
permission: {
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
{
items: [
{
id: '123',
resourceRefs: [],
permission: {
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
{
items: [
{
id: '123',
resourceRefs: ['resource:1'],
resourceRef: 'resource:1',
permission: {
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
{
items: [
{
id: '123',
resourceRefs: ['resource:1'],
permission: {
type: 'basic',
name: 'test.permission',
@@ -966,16 +894,17 @@ describe('createRouter', () => {
items: [
{
id: '123',
resourceRef: 'resource:1',
resourceRef: [],
permission: {
type: 'basic',
type: 'resource',
name: 'test.permission',
attributes: {},
resourceType: 'test-resource-1',
},
},
],
},
])('returns a 400 error for invalid request %#', async requestBody => {
])('returns a 400 error for invalid request %o', async requestBody => {
const response = await request(app).post('/authorize').send(requestBody);
expect(response.status).toEqual(400);
@@ -1017,7 +946,7 @@ describe('createRouter', () => {
);
});
it(`returns a 400 error if the request doesn't contain resourceRef or resourceRefs for credentials not issued by a service`, async () => {
it(`returns a 400 error if the request doesn't contain resourceRef for credentials not issued by a service`, async () => {
policy.handle.mockResolvedValueOnce({
result: AuthorizeResult.CONDITIONAL,
pluginId: 'test-plugin',
@@ -76,19 +76,13 @@ const evaluatePermissionRequestSchema = z.union([
z.object({
id: z.string(),
resourceRef: z.undefined().optional(),
resourceRefs: z.undefined().optional(),
permission: basicPermissionSchema,
}),
z.object({
id: z.string(),
resourceRef: z.string().optional(),
resourceRefs: z.undefined().optional(),
permission: resourcePermissionSchema,
}),
z.object({
id: z.string(),
resourceRef: z.undefined().optional(),
resourceRefs: z.array(z.string()).nonempty().optional(),
resourceRef: z
.union([z.string(), z.array(z.string()).nonempty()])
.optional(),
permission: resourcePermissionSchema,
}),
]);
@@ -178,14 +172,6 @@ const handleRequest = async (
);
}
if (request.resourceRefs) {
return applyConditionsLoaderFor(decision.pluginId).load({
id: request.id,
resourceRefs: request.resourceRefs,
...decision,
});
}
if (!request.resourceRef) {
return {
id: request.id,
@@ -265,9 +251,7 @@ export async function createRouter(
if (
body.items.some(
r =>
isResourcePermission(r.permission) &&
r.resourceRef === undefined &&
r.resourceRefs === undefined,
isResourcePermission(r.permission) && r.resourceRef === undefined,
)
) {
throw new InputError(
@@ -285,7 +285,7 @@ describe('PermissionClient', () => {
{
id: expect.any(String),
permission: mockPermission,
resourceRefs: ['foo:bar', 'foo:car', 'foo:baz'],
resourceRef: ['foo:bar', 'foo:car', 'foo:baz'],
},
{
id: expect.any(String),
@@ -453,7 +453,7 @@ describe('PermissionClient', () => {
attributes: {},
resourceType: 'foo',
},
resourceRefs: ['foo:bar', 'foo:car'],
resourceRef: ['foo:bar', 'foo:car'],
id: expect.any(String),
},
{
@@ -471,7 +471,7 @@ describe('PermissionClient', () => {
attributes: {},
resourceType: 'foo',
},
resourceRefs: ['r2', 'r1'],
resourceRef: ['r2', 'r1'],
id: expect.any(String),
},
],
@@ -210,12 +210,12 @@ export class PermissionClient implements PermissionEvaluator {
if (isResourcePermission(permission)) {
request[permission.name] ||= {
permission,
resourceRefs: [],
resourceRef: [],
id: uuid.v4(),
};
if (resourceRef) {
request[permission.name].resourceRefs?.push(resourceRef);
request[permission.name].resourceRef?.push(resourceRef);
}
} else {
request[permission.name] ||= {
@@ -231,10 +231,15 @@ export class PermissionClient implements PermissionEvaluator {
options,
);
const responsesById = parsedResponse.items.reduce((acc, r) => {
acc[r.id] = r;
return acc;
}, {} as Record<string, (typeof parsedResponse)['items'][number]>);
return queries.map(query => {
const { id } = request[query.permission.name];
const item = parsedResponse.items.find(i => i.id === id)!;
const item = responsesById[id];
return {
result: query.resourceRef ? item.result.shift()! : item.result[0],
};
@@ -278,7 +283,7 @@ export class PermissionClient implements PermissionEvaluator {
export type BatchedAuthorizePermissionRequest = IdentifiedPermissionMessage<
| {
permission: BasicPermission;
resourceRefs?: undefined;
resourceRef?: undefined;
}
| { permission: ResourcePermission; resourceRefs: string[] }
| { permission: ResourcePermission; resourceRef: string[] }
>;
+5 -14
View File
@@ -38,20 +38,11 @@ export type ApplyConditionsRequest = {
};
// @public
export type ApplyConditionsRequestEntry = IdentifiedPermissionMessage<
| {
resourceRef: string;
resourceRefs?: undefined;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}
| {
resourceRef?: undefined;
resourceRefs: string[];
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}
>;
export type ApplyConditionsRequestEntry = IdentifiedPermissionMessage<{
resourceRef: string | string[];
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}>;
// @public
export type ApplyConditionsResponse = {
@@ -567,7 +567,7 @@ describe('createPermissionIntegrationRouter', () => {
});
});
describe('batched requests with resourceRefs', () => {
describe('batched requests with resourceRef as an array', () => {
let response: Response;
beforeEach(async () => {
@@ -587,7 +587,7 @@ describe('createPermissionIntegrationRouter', () => {
items: [
{
id: '123',
resourceRefs: [
resourceRef: [
'default:test/resource-1',
'default:test/resource-2',
],
@@ -58,22 +58,12 @@ const permissionCriteriaSchema: z.ZodSchema<
const applyConditionsRequestSchema = z.object({
items: z.array(
z.union([
z.object({
id: z.string(),
resourceRef: z.string(),
resourceRefs: z.undefined().optional(),
resourceType: z.string(),
conditions: permissionCriteriaSchema,
}),
z.object({
id: z.string(),
resourceRef: z.undefined().optional(),
resourceRefs: z.array(z.string()),
resourceType: z.string(),
conditions: permissionCriteriaSchema,
}),
]),
z.object({
id: z.string(),
resourceRef: z.union([z.string(), z.array(z.string()).nonempty()]),
resourceType: z.string(),
conditions: permissionCriteriaSchema,
}),
),
});
@@ -83,20 +73,11 @@ const applyConditionsRequestSchema = z.object({
*
* @public
*/
export type ApplyConditionsRequestEntry = IdentifiedPermissionMessage<
| {
resourceRef: string;
resourceRefs?: undefined;
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}
| {
resourceRef?: undefined;
resourceRefs: string[];
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}
>;
export type ApplyConditionsRequestEntry = IdentifiedPermissionMessage<{
resourceRef: string | string[];
resourceType: string;
conditions: PermissionCriteria<PermissionCondition>;
}>;
/**
* A batch of {@link ApplyConditionsRequestEntry} objects.
@@ -536,7 +517,7 @@ export function createPermissionIntegrationRouter<
requestedType,
requests
.filter(r => r.resourceType === requestedType)
.map(i => i.resourceRefs ?? [i.resourceRef])
.map(i => i.resourceRef)
.flat(),
);
}
@@ -544,8 +525,8 @@ export function createPermissionIntegrationRouter<
res.json({
items: requests.map(request => ({
id: request.id,
result: request.resourceRefs
? request.resourceRefs.map(resourceRef =>
result: Array.isArray(request.resourceRef)
? request.resourceRef.map(resourceRef =>
authorizeResult(
request.conditions,
resourcesByType[request.resourceType][resourceRef],