make sure that permissions apply
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog';
|
||||
import { Cursor, QueryEntitiesResponse } from '../catalog/types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityFilter } from '@backstage/plugin-catalog-node';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import { mockCredentials } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('AuthorizedEntitiesCatalog', () => {
|
||||
@@ -306,6 +307,177 @@ describe('AuthorizedEntitiesCatalog', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('combines permission filter into query field using $all on CONDITIONAL with initial request', async () => {
|
||||
fakePermissionApi.authorizeConditional.mockResolvedValue([
|
||||
{
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
conditions: {
|
||||
rule: 'IS_ENTITY_KIND',
|
||||
params: { kinds: ['b'] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const userQuery: FilterPredicate = { 'metadata.name': 'my-entity' };
|
||||
|
||||
const entities = [
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
name: 'a',
|
||||
} as unknown as Entity,
|
||||
];
|
||||
|
||||
fakeCatalog.queryEntities.mockResolvedValue({
|
||||
items: { type: 'object', entities },
|
||||
pageInfo: {
|
||||
nextCursor: {
|
||||
isPrevious: false,
|
||||
orderFieldValues: ['xxx', null],
|
||||
query: { $all: [{ kind: 'b' }, userQuery] },
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
},
|
||||
},
|
||||
totalItems: 1,
|
||||
} as QueryEntitiesResponse);
|
||||
|
||||
const catalog = createCatalog(isEntityKind);
|
||||
|
||||
const response = await catalog.queryEntities({
|
||||
credentials: mockCredentials.none(),
|
||||
query: userQuery,
|
||||
});
|
||||
|
||||
expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({
|
||||
credentials: mockCredentials.none(),
|
||||
query: { $all: [{ kind: 'b' }, userQuery] },
|
||||
filter: undefined,
|
||||
});
|
||||
|
||||
expect(response.pageInfo.nextCursor).toEqual({
|
||||
isPrevious: false,
|
||||
orderFieldValues: ['xxx', null],
|
||||
query: userQuery,
|
||||
filter: undefined,
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('combines permission filter into cursor query field using $all on CONDITIONAL with cursor request', async () => {
|
||||
fakePermissionApi.authorizeConditional.mockResolvedValue([
|
||||
{
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
conditions: {
|
||||
rule: 'IS_ENTITY_KIND',
|
||||
params: { kinds: ['b'] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const userQuery: FilterPredicate = { 'metadata.name': 'my-entity' };
|
||||
|
||||
const entities = [
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'default',
|
||||
name: 'a',
|
||||
} as unknown as Entity,
|
||||
];
|
||||
|
||||
fakeCatalog.queryEntities.mockResolvedValue({
|
||||
items: { type: 'object', entities },
|
||||
pageInfo: {
|
||||
nextCursor: {
|
||||
isPrevious: false,
|
||||
orderFieldValues: ['yyy', null],
|
||||
query: { $all: [{ kind: 'b' }, userQuery] },
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
},
|
||||
prevCursor: {
|
||||
isPrevious: true,
|
||||
orderFieldValues: ['aaa', null],
|
||||
query: { $all: [{ kind: 'b' }, userQuery] },
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
},
|
||||
},
|
||||
totalItems: 3,
|
||||
} as QueryEntitiesResponse);
|
||||
|
||||
const catalog = createCatalog(isEntityKind);
|
||||
|
||||
const cursor: Cursor = {
|
||||
query: userQuery,
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
isPrevious: false,
|
||||
orderFieldValues: ['xxx', null],
|
||||
};
|
||||
|
||||
const response = await catalog.queryEntities({
|
||||
credentials: mockCredentials.none(),
|
||||
cursor,
|
||||
});
|
||||
|
||||
expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({
|
||||
credentials: mockCredentials.none(),
|
||||
cursor: {
|
||||
...cursor,
|
||||
query: { $all: [{ kind: 'b' }, userQuery] },
|
||||
filter: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.pageInfo.nextCursor).toEqual({
|
||||
isPrevious: false,
|
||||
orderFieldValues: ['yyy', null],
|
||||
query: userQuery,
|
||||
filter: undefined,
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
});
|
||||
|
||||
expect(response.pageInfo.prevCursor).toEqual({
|
||||
isPrevious: true,
|
||||
orderFieldValues: ['aaa', null],
|
||||
query: userQuery,
|
||||
filter: undefined,
|
||||
orderFields: [{ field: 'name', order: 'asc' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts multi-value permission filter with $in when converting to predicate', async () => {
|
||||
fakePermissionApi.authorizeConditional.mockResolvedValue([
|
||||
{
|
||||
result: AuthorizeResult.CONDITIONAL,
|
||||
conditions: {
|
||||
rule: 'IS_ENTITY_KIND',
|
||||
params: { kinds: ['component', 'api'] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const userQuery: FilterPredicate = { 'metadata.name': 'my-entity' };
|
||||
|
||||
fakeCatalog.queryEntities.mockResolvedValue({
|
||||
items: { type: 'object', entities: [] },
|
||||
pageInfo: {},
|
||||
totalItems: 0,
|
||||
} as QueryEntitiesResponse);
|
||||
|
||||
const catalog = createCatalog(isEntityKind);
|
||||
|
||||
await catalog.queryEntities({
|
||||
credentials: mockCredentials.none(),
|
||||
query: userQuery,
|
||||
});
|
||||
|
||||
expect(fakeCatalog.queryEntities).toHaveBeenCalledWith({
|
||||
credentials: mockCredentials.none(),
|
||||
query: {
|
||||
$all: [{ kind: { $in: ['component', 'api'] } }, userQuery],
|
||||
},
|
||||
filter: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEntityByUid', () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
} from '../catalog/types';
|
||||
import { FilterPredicate } from '@backstage/filter-predicates';
|
||||
import { basicEntityFilter } from './request';
|
||||
import { isQueryEntitiesCursorRequest } from './util';
|
||||
import { EntityFilter } from '@backstage/plugin-catalog-node';
|
||||
@@ -43,6 +44,30 @@ import {
|
||||
PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
function entityFilterToFilterPredicate(filter: EntityFilter): FilterPredicate {
|
||||
if ('allOf' in filter) {
|
||||
return { $all: filter.allOf.map(entityFilterToFilterPredicate) };
|
||||
}
|
||||
|
||||
if ('anyOf' in filter) {
|
||||
return { $any: filter.anyOf.map(entityFilterToFilterPredicate) };
|
||||
}
|
||||
|
||||
if ('not' in filter) {
|
||||
return { $not: entityFilterToFilterPredicate(filter.not) };
|
||||
}
|
||||
|
||||
if (!filter.values) {
|
||||
return { [filter.key]: { $exists: true } } as FilterPredicate;
|
||||
}
|
||||
|
||||
if (filter.values.length === 1) {
|
||||
return { [filter.key]: filter.values[0] } as FilterPredicate;
|
||||
}
|
||||
|
||||
return { [filter.key]: { $in: filter.values } } as FilterPredicate;
|
||||
}
|
||||
|
||||
export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
|
||||
private readonly entitiesCatalog: EntitiesCatalog;
|
||||
private readonly permissionApi: PermissionsService;
|
||||
@@ -147,18 +172,42 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
|
||||
|
||||
let permissionedRequest: QueryEntitiesRequest;
|
||||
let requestFilter: EntityFilter | undefined;
|
||||
let requestQuery: FilterPredicate | undefined;
|
||||
|
||||
if (isQueryEntitiesCursorRequest(request)) {
|
||||
requestFilter = request.cursor.filter;
|
||||
requestQuery = request.cursor.query;
|
||||
|
||||
if (request.cursor.query) {
|
||||
const permissionPredicate =
|
||||
entityFilterToFilterPredicate(permissionFilter);
|
||||
permissionedRequest = {
|
||||
...request,
|
||||
cursor: {
|
||||
...request.cursor,
|
||||
query: { $all: [permissionPredicate, request.cursor.query] },
|
||||
filter: undefined,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
permissionedRequest = {
|
||||
...request,
|
||||
cursor: {
|
||||
...request.cursor,
|
||||
filter: request.cursor.filter
|
||||
? { allOf: [permissionFilter, request.cursor.filter] }
|
||||
: permissionFilter,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else if (request.query) {
|
||||
const permissionPredicate =
|
||||
entityFilterToFilterPredicate(permissionFilter);
|
||||
requestQuery = request.query;
|
||||
permissionedRequest = {
|
||||
...request,
|
||||
cursor: {
|
||||
...request.cursor,
|
||||
filter: request.cursor.filter
|
||||
? { allOf: [permissionFilter, request.cursor.filter] }
|
||||
: permissionFilter,
|
||||
},
|
||||
query: { $all: [permissionPredicate, request.query] },
|
||||
filter: undefined,
|
||||
};
|
||||
} else {
|
||||
permissionedRequest = {
|
||||
@@ -177,11 +226,13 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
|
||||
const prevCursor: Cursor | undefined = response.pageInfo.prevCursor && {
|
||||
...response.pageInfo.prevCursor,
|
||||
filter: requestFilter,
|
||||
query: requestQuery,
|
||||
};
|
||||
|
||||
const nextCursor: Cursor | undefined = response.pageInfo.nextCursor && {
|
||||
...response.pageInfo.nextCursor,
|
||||
filter: requestFilter,
|
||||
query: requestQuery,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -193,6 +244,7 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog {
|
||||
};
|
||||
}
|
||||
|
||||
// The ALLOW case
|
||||
return this.entitiesCatalog.queryEntities(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export function applyPredicateEntityFilterToQuery(options: {
|
||||
|
||||
if ('$all' in filter) {
|
||||
if (filter.$all.length === 0) {
|
||||
return targetQuery;
|
||||
return targetQuery.andWhereRaw('1 = 1');
|
||||
}
|
||||
return targetQuery.andWhere(outer => {
|
||||
for (const subFilter of filter.$all) {
|
||||
|
||||
Reference in New Issue
Block a user