Skip to content

Commit

Permalink
Fix using $filter on PATCH&DELETE requests on translated properties
Browse files Browse the repository at this point in the history
  • Loading branch information
thgreasi committed Aug 30, 2024
1 parent 8c2f66a commit 8f29909
Showing 1 changed file with 77 additions and 2 deletions.
79 changes: 77 additions & 2 deletions src/sbvr-api/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ const methodPermissions: {
DELETE: 'delete',
};

const stringifiedMethodPermissions = Object.fromEntries(
Object.entries(methodPermissions)
.filter((p) => p[1] != null)
.map((p) => [p[0], JSON.stringify(p[1])]),
) as Record<keyof typeof methodPermissions, string>;

const $parsePermissions = env.createCache(
'parsePermissions',
(filter: string) => {
Expand Down Expand Up @@ -742,6 +748,38 @@ const createBypassDefinition = (definition: Definition) =>
}
});

const createVersionSpecificPermissionDefinition = (
{ abstractSql, ...restDefinition }: Definition,
permissionsJSON: string,
) => {
return {
..._.cloneDeep(restDefinition),
abstractSql: _.cloneDeepWith(abstractSql, (abstractSqlNode) => {
if (!Array.isArray(abstractSqlNode)) {
// Don't affect how non-array nodes get cloned.
return;
}
if (abstractSqlNode[0] === 'Select' || abstractSqlNode[0] === 'Where') {
// We only want to rewrite Resources that are under a From node, so we use
// cloneDeep so that cloneDeepWith is not called again for their sub-tree.
// Eg: when we have computed terms under a Select node, we don't rewrite them
// so that the read permissions are still used even in PATCH/DELETEs.
return _.cloneDeep(abstractSqlNode);
}
if (
abstractSqlNode[0] === 'Resource' &&
typeof abstractSqlNode[1] === 'string' &&
// Do not rewrite Resources that already specify which permissions need to be used (or bypassed).
!abstractSqlNode[1].includes('$permissions') &&
!abstractSqlNode[1].endsWith('$bypass')
) {
const resource = abstractSqlNode[1];
return ['Resource', `${resource}$permissions${permissionsJSON}`];
}
}),
};
};

const getAlias = (name: string) => {
// TODO-MAJOR: Change $bypass to $permissionbypass or similar
if (name.endsWith('$bypass')) {
Expand Down Expand Up @@ -955,7 +993,6 @@ const rewriteRelationships = (
return newRelationships;
};

const stringifiedGetPermissions = JSON.stringify(methodPermissions.GET);
const getBoundConstrainedMemoizer = memoizeWeak(
(abstractSqlModel: AbstractSqlModel) =>
memoizeWeak(
Expand Down Expand Up @@ -1002,16 +1039,54 @@ const getBoundConstrainedMemoizer = memoizeWeak(
// If the table is definition based then just make the bypass version match but pointing to the equivalent bypassed resources
constrainedAbstractSqlModel.tables[bypassResourceName].definition =
createBypassDefinition(table.definition);

// Make clear to TS that table.definition isn't going to become undefined by the time we use it inside the onceGetter below.
const tableDefinition = table.definition;

// For the translated resources, we create a fake `resource$permissions"<permJson>"` resource
// and replace their nested Resource references to also point to that parallel `$permissions"<permJson>"` suffixed model
// so that once the whole chain of `resource$permissions"<permJson>"` -> `resource$vX$permissions"<permJson>"`-> `resource$vX+1$permissions"<permJson>"` Resource references
// is fully resolved, the permissions are applied to the last model in the chain.
// The permissions are added to the last model in the chain eg `resource$vDB$permissions"<permJson>"`,
// by the generateConstrainedAbstractSql() that's in the `onceGetter(permissionsTable, 'definition'` below.
for (const stringifiedPermission of _.uniq(
Object.values(stringifiedMethodPermissions),
)) {
if (stringifiedPermission === stringifiedMethodPermissions.GET) {
// The read permissions (for GET requests) are resolved using the plain resources in the constrainedAbstractSqlModel
// (those without a `$permissions` suffix, which get generated by the memoizedGetConstrainedModel),
// so we don't need to re-create a set of `<resource>$permissions"read"` resources.
continue;
}
// If the permission is not a GET then we need to create a permissions table
const permissionResourceName = `${resourceName}$permissions${stringifiedPermission}`;
constrainedAbstractSqlModel.tables[permissionResourceName] = {
...table,
resourceName: permissionResourceName,
};
onceGetter(
constrainedAbstractSqlModel.tables[permissionResourceName],
'definition',
() =>
createVersionSpecificPermissionDefinition(
tableDefinition,
stringifiedPermission,
),
);
}
} else {
// Otherwise constrain the non-bypass table
// When the table doesn't have a definition, then it's the last translation layer,
// in which case we define it as the virtual `resource$vDB$permissions"read"` resource,
// which invokes the generateConstrainedAbstractSql() that's in the `onceGetter(permissionsTable, 'definition'` below
onceGetter(
table,
'definition',
() =>
// For $filter on eg a DELETE you need read permissions on the sub-resources,
// you only need delete permissions on the resource being deleted
constrainedAbstractSqlModel.tables[
`${resourceName}$permissions${stringifiedGetPermissions}`
`${resourceName}$permissions${stringifiedMethodPermissions.GET}`
].definition,
);
}
Expand Down

0 comments on commit 8f29909

Please sign in to comment.