Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/agent/src/routes/access/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ export default class ChartRoute extends CollectionRoute {
}

private async getFilter(context: Context): Promise<Filter> {
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);

const scope = await this.services.authorization.getScope(this.collection, context);

return ContextFilterFactory.build(this.collection, context, scope);
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/count-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default class CountRelatedRoute extends RelationRoute {

public async handleCountRelated(context: Context): Promise<void> {
await this.services.authorization.assertCanBrowse(context, this.foreignCollection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.foreignCollection.name,
);

if (this.foreignCollection.schema.countable) {
await this.services.authorization.assertCanReadQueryFields(context, this.foreignCollection, [
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default class CountRoute extends CollectionRoute {

public async handleCount(context: Context): Promise<void> {
await this.services.authorization.assertCanBrowse(context, this.collection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);

if (this.collection.schema.countable) {
await this.services.authorization.assertCanReadQueryFields(context, this.collection, [
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/csv-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default class CsvRelatedRoute extends RelationRoute {

async handleRelatedCsv(context: Context): Promise<void> {
await this.services.authorization.assertCanBrowse(context, this.foreignCollection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.foreignCollection.name,
);
await this.services.authorization.assertCanExport(context, this.foreignCollection.name);
await this.services.authorization.assertCanReadQueryFields(context, this.foreignCollection);

Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default class CsvRoute extends CollectionRoute {

async handleCsv(context: Context): Promise<void> {
await this.services.authorization.assertCanBrowse(context, this.collection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);
await this.services.authorization.assertCanExport(context, this.collection.name);
await this.services.authorization.assertCanReadQueryFields(context, this.collection);

Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/list-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default class ListRelatedRoute extends RelationRoute {

public async handleListRelated(context: Context): Promise<void> {
await this.services.authorization.assertCanBrowse(context, this.foreignCollection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.foreignCollection.name,
);
await this.services.authorization.assertCanReadQueryFields(context, this.foreignCollection);

const parentId = IdUtils.unpackId(this.collection.schema, context.params.parentId);
Expand Down
4 changes: 4 additions & 0 deletions packages/agent/src/routes/access/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export default class ListRoute extends CollectionRoute {

public async handleList(context: Context) {
await this.services.authorization.assertCanBrowse(context, this.collection.name);
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);
await this.services.authorization.assertCanReadQueryFields(context, this.collection);

const scope = await this.services.authorization.getScope(this.collection, context);
Expand Down
28 changes: 22 additions & 6 deletions packages/agent/src/routes/modification/action/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,18 +384,34 @@ export default class ActionRoute extends CollectionRoute {
const body = context.request.body as any;
const attributes = body?.data?.attributes;

const actionScope = this.collection.schema.actions[this.actionName].scope;
const selectionIds =
actionScope === 'Global'
? null
: BodyParser.parseSelectionIds(this.collection.schema, context);
// Explicit ids already pin the records: the selection's segment adds nothing, so it never runs.
const isExplicitIdSelection = selectionIds !== null && !selectionIds.areExcluded;

// Match user filter + search + scope? + segment.
if (!isExplicitIdSelection) {
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);
}

const scope = includeUserScope
? await this.services.authorization.getScope(this.collection, context)
: null;
let filter = ContextFilterFactory.build(this.collection, context, scope);
let filter = ContextFilterFactory.build(
this.collection,
context,
scope,
isExplicitIdSelection ? { liveQuerySegment: undefined } : undefined,
);

// Restrict the filter to the selected records for single or bulk actions.
const actionScope = this.collection.schema.actions[this.actionName].scope;

if (actionScope !== 'Global') {
const selectionIds = BodyParser.parseSelectionIds(this.collection.schema, context);

if (selectionIds) {
// A single action runs on its first record, or on nothing: refuse any selection but one id.
if (actionScope === 'Single' && (selectionIds.areExcluded || selectionIds.ids.length !== 1)) {
throw new UnprocessableError('This action can only be triggered on one record at a time.');
Expand Down
5 changes: 5 additions & 0 deletions packages/agent/src/routes/modification/associate-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export default class AssociateRelatedRoute extends RelationRoute {
targetedRelationId: CompositeId,
context: Context,
) {
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.foreignCollection.name,
);

const [id] = SchemaUtils.getPrimaryKeys(this.foreignCollection.schema);
let value = await CollectionUtils.getValue(
this.foreignCollection,
Expand Down
5 changes: 5 additions & 0 deletions packages/agent/src/routes/modification/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export default class DeleteRoute extends CollectionRoute {
}

private async deleteRecords(context: Context, selectionIds: SelectionIds): Promise<void> {
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.collection.name,
);

let selectedIds = ConditionTreeFactory.matchIds(this.collection.schema, selectionIds.ids);
if (selectionIds.areExcluded) selectedIds = selectedIds.inverse();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export default class DissociateDeleteRelatedRoute extends RelationRoute {
* The filter that is generated by this condition it _not_ restricted by the parent record
*/
private async getBaseForeignFilter(context: Context): Promise<Filter> {
await this.services.authorization.assertCanExecuteLiveQuerySegment(
context,
this.foreignCollection.name,
);

const selectionIds = BodyParser.parseSelectionIds(this.foreignCollection.schema, context);
let selectedIds = ConditionTreeFactory.matchIds(
this.foreignCollection.schema,
Expand Down
47 changes: 24 additions & 23 deletions packages/agent/src/services/authorization/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,29 @@ export default class AuthorizationService {
}
}

public async assertCanExecuteLiveQuerySegment(
context: Context,
collectionName: string,
): Promise<void> {
const liveQuerySegment = context.request
? QueryStringParser.parseLiveQuerySegment(context)
: null;

if (!liveQuerySegment) return;

const { id: userId, renderingId } = context.state.user;
const canExecuteSegmentQuery =
await this.forestAdminClient.permissionService.canExecuteSegmentQuery({
userId,
collectionName,
renderingId,
segmentQuery: liveQuerySegment.query,
connectionName: liveQuerySegment.connectionName,
});

if (!canExecuteSegmentQuery) context.throw(HttpCode.Forbidden, 'Forbidden');
}

/** The root is skipped: `browse` gates a listing, `read` a get, and the signed hash a chart. */
private async getReadPermissions(
context: Context,
Expand All @@ -207,36 +230,14 @@ export default class AuthorizationService {
context: Context,
collectionName: string,
) {
const { id: userId, renderingId } = context.state.user;
const { id: userId } = context.state.user;

const canOnCollection = await this.forestAdminClient.permissionService.canOnCollection({
userId,
event,
collectionName,
});

if (
context.request?.query &&
CollectionActionEvent.Browse === event &&
(context.request.query as { segmentQuery?: string }).segmentQuery
) {
const { segmentQuery, connectionName } = context.request.query as {
segmentQuery?: string;
connectionName?: string;
};

const canExecuteSegmentQuery =
await this.forestAdminClient.permissionService.canExecuteSegmentQuery({
userId,
collectionName,
renderingId,
segmentQuery,
connectionName,
});

if (!canExecuteSegmentQuery) context.throw(HttpCode.Forbidden, 'Forbidden');
}

if (!canOnCollection) {
context.throw(HttpCode.Forbidden, 'Forbidden');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class AuthorizationsFactory extends Factory<AuthorizationService> {
Authorizations.assertCanExport = jest.fn();
Authorizations.getScope = jest.fn();
Authorizations.assertCanExecuteChart = jest.fn();
Authorizations.assertCanExecuteLiveQuerySegment = jest.fn();
Authorizations.invalidateScopeCache = jest.fn();
Authorizations.canRead = jest.fn().mockResolvedValue(true);
Authorizations.assertCanReadQueryFields = jest.fn();
Expand Down
Loading
Loading