Skip to content

Commit

Permalink
Merge pull request #92 from aspen-cloud/matlin/cyclical-permissions
Browse files Browse the repository at this point in the history
Support cyclical permissions
  • Loading branch information
matlin authored and wernst committed Jan 3, 2025
1 parent a23fcc9 commit 60e3078
Show file tree
Hide file tree
Showing 5 changed files with 378 additions and 9 deletions.
5 changes: 2 additions & 3 deletions packages/db/src/collection-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,7 @@ export async function loadSubquery(
} as CollectionQuery<any, any>;

fullSubquery = prepareQuery(fullSubquery, schema, options.session, {
skipRules: options.skipRules,
skipRules: true,
});

// Push entity onto context stack
Expand Down Expand Up @@ -1475,6 +1475,7 @@ export async function loadQuery<
executionContext,
options
);

const { order, limit, after } = queryWithInsertedVars;

// Load possible entity ids from indexes
Expand Down Expand Up @@ -2405,7 +2406,6 @@ export async function replaceVariablesInQuery<Q extends CollectionQuery<any>>(
options: FetchFromStorageOptions
): Promise<Q> {
const clauses = (query.where ?? []).filter(isFilterStatement);

for (const clause of clauses) {
const val = clause[2];
if (isValueReferentialVariable(val)) {
Expand All @@ -2420,7 +2420,6 @@ export async function replaceVariablesInQuery<Q extends CollectionQuery<any>>(
}

const vars = getQueryVariables(query, executionContext, options);

const where = query.where
? replaceVariablesInFilterStatements(query.where, vars)
: undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/db/src/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ export function replaceVariablesInFilterStatements<
variables: Record<string, any>
): QueryWhere<M, CN> {
return statements.map((filter) => {
if (isFilterGroup(filter))
if (isFilterGroup(filter)) {
return {
...filter,
filters: replaceVariablesInFilterStatements(filter.filters, variables),
};
}
if (isFilterStatement(filter)) {
const replacedValue = replaceVariable(filter[2], variables);
return [filter[0], filter[1], replacedValue];
Expand Down
30 changes: 26 additions & 4 deletions packages/db/src/query/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export function prepareQuery<M extends Models, Q extends CollectionQuery<M>>(
query: Q,
schema: M | undefined,
session: Session,
options: QueryPreparationOptions = {
options: QueryPreparationOptions & {
collectionsPermissionsChecked?: Set<string>;
} = {
skipRules: false,
bindSessionVariables: false,
}
Expand All @@ -79,7 +81,13 @@ export function prepareQuery<M extends Models, Q extends CollectionQuery<M>>(
const include = getQueryInclude(fetchQuery, schema, session, options);

// Determine filters
const where = getQueryFilters(fetchQuery, schema, session, options);
const isPermissionAlreadyChecked = options.collectionsPermissionsChecked?.has(
fetchQuery.collectionName
);
const where = getQueryFilters(fetchQuery, schema, session, {
...options,
skipRules: isPermissionAlreadyChecked || options.skipRules,
});

// Determine order
const order = getQueryOrder(fetchQuery, schema, options);
Expand Down Expand Up @@ -353,7 +361,14 @@ function getQueryFilters<M extends Models, Q extends CollectionQuery<M>>(
];

return {
exists: prepareQuery(subquery, schema, session, options),
exists: prepareQuery(subquery, schema, session, {
...options,
collectionsPermissionsChecked: new Set([
// @ts-expect-error
...(options.collectionsPermissionsChecked ?? []),
query.collectionName,
]),
}),
};
}
if (!Array.isArray(statement)) return statement;
Expand All @@ -376,7 +391,14 @@ function getQueryFilters<M extends Models, Q extends CollectionQuery<M>>(
}
subquery.where = [...subquery.where, [path.join('.'), op, val]];
return {
exists: prepareQuery(subquery, schema, session, options),
exists: prepareQuery(subquery, schema, session, {
...options,
collectionsPermissionsChecked: new Set([
// @ts-expect-error
...(options.collectionsPermissionsChecked ?? []),
query.collectionName,
]),
}),
};
}
}
Expand Down
127 changes: 127 additions & 0 deletions packages/db/test/db.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5892,3 +5892,130 @@ describe('variable conflicts', () => {
});
});
});

describe('cyclical permissions', () => {
const schema: Models = {
users: {
schema: S.Schema({
id: S.Id(),
}),
},
events: {
schema: S.Schema({
id: S.Id(),
attendees: S.RelationMany('eventAttendees', {
where: [['eventId', '=', '$id']],
}),
}),
permissions: {
user: {
read: {
// Can see events that they are attending
filter: [['attendees.userId', '=', '$role.user_id']],
},
},
},
},
eventAttendees: {
schema: S.Schema({
id: S.Id(),
eventId: S.String(),
event: S.RelationById('events', '$eventId'),
userId: S.String(),
user: S.RelationById('users', '$userId'),
}),
permissions: {
user: {
read: {
// Can see event attendees of events that they are attending
filter: [['event.attendees.userId', '=', '$role.user_id']],
},
},
},
},
};

const db = new DB({
schema: {
roles: {
user: {
match: {
role: 'user',
userId: '$user_id',
},
},
},
collections: schema,
version: 0,
},
});

beforeAll(async () => {
await db.insert('users', { id: '1' }, { skipRules: true });
await db.insert('users', { id: '2' }, { skipRules: true });
await db.insert('users', { id: '3' }, { skipRules: true });
await db.insert('users', { id: '4' }, { skipRules: true });
await db.insert('events', { id: '1' }, { skipRules: true });
await db.insert('events', { id: '2' }, { skipRules: true });
await db.insert(
'eventAttendees',
{ id: '1', eventId: '1', userId: '1' },
{ skipRules: true }
);
await db.insert(
'eventAttendees',
{ id: '2', eventId: '1', userId: '2' },
{ skipRules: true }
);
await db.insert(
'eventAttendees',
{ id: '3', eventId: '2', userId: '1' },
{ skipRules: true }
);
await db.insert(
'eventAttendees',
{ id: '4', eventId: '2', userId: '3' },
{ skipRules: true }
);
await db.insert(
'eventAttendees',
{ id: '5', eventId: '2', userId: '4' },
{ skipRules: true }
);
});
const user1Session = db.withSessionVars({
role: 'user',
userId: '1',
});
const user2Session = db.withSessionVars({
role: 'user',
userId: '2',
});

it('can query events that a user is attending', async () => {
{
const result = await user1Session.fetch(db.query('events').build());
expect(result.length).toBe(2);
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
}
{
const result = await user2Session.fetch(db.query('events').build());
expect(result.length).toBe(1);
expect(result).toEqual([{ id: '1' }]);
}
});
it('can query mutual attendees', async () => {
{
const result = await user1Session.fetch(
db.query('eventAttendees').build()
);
expect(result.length).toBe(5);
}
{
const result = await user2Session.fetch(
db.query('eventAttendees').build()
);
expect(result.length).toBe(2);
}
});
});
Loading

0 comments on commit 60e3078

Please sign in to comment.