-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/restrict metadata #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
23e44a0
1f581e7
2f3a22d
628773d
9b1657b
0b9cfe8
726c8c9
55573b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,8 @@ import type { ZodTypeProvider } from 'fastify-type-provider-zod'; | |
| import { z } from 'zod/v4'; | ||
| import type { PrismaClient } from '../generated/prisma/client.js'; | ||
| import type { FileMetadata, RoCrateHandler } from '../types/fileHandlers.js'; | ||
| import { createInternalError, createNotFoundError } from '../utils/errors.js'; | ||
| import type { AccessTransformer } from '../types/transformers.js'; | ||
| import { createForbiddenError, createInternalError, createNotFoundError } from '../utils/errors.js'; | ||
| import { setFileHeaders } from '../utils/headers.js'; | ||
|
|
||
| const paramsSchema = z.object({ | ||
|
|
@@ -13,11 +14,12 @@ const paramsSchema = z.object({ | |
|
|
||
| type CrateRouteOptions = { | ||
| prisma: PrismaClient; | ||
| accessTransformer: AccessTransformer; | ||
| roCrateHandler: RoCrateHandler; | ||
| }; | ||
|
|
||
| const crate: FastifyPluginAsync<CrateRouteOptions> = async (fastify, opts) => { | ||
| const { prisma, roCrateHandler } = opts; | ||
| const { prisma, accessTransformer, roCrateHandler } = opts; | ||
|
|
||
| fastify.withTypeProvider<ZodTypeProvider>().head( | ||
| '/entity/:id/rocrate', | ||
|
|
@@ -37,9 +39,17 @@ const crate: FastifyPluginAsync<CrateRouteOptions> = async (fastify, opts) => { | |
| if (!entity) { | ||
| return reply.code(404).send(createNotFoundError('The requested entity was not found', id)); | ||
| } | ||
| const standardEntity = { | ||
| ...entity, | ||
| memberOf: { id: entity.memberOf || '', name: '' }, | ||
| rootCollection: { id: entity.rootCollection || '', name: '' }, | ||
| }; | ||
| const authorisedEntity = await accessTransformer(standardEntity, { request, fastify }); | ||
| if (!authorisedEntity.access.metadata) { | ||
| return reply.code(403).send(createForbiddenError('Access to this resource is restricted')); | ||
| } | ||
|
|
||
| const metadata: FileMetadata | false = await roCrateHandler.head(entity, { request, fastify }); | ||
|
|
||
| if (!metadata) { | ||
| return reply.code(404).send(createNotFoundError('The requested RO-Crate metadata was not found', id)); | ||
| } | ||
|
|
@@ -75,6 +85,15 @@ const crate: FastifyPluginAsync<CrateRouteOptions> = async (fastify, opts) => { | |
| return reply.code(404).send(createNotFoundError('The requested entity was not found', id)); | ||
| } | ||
|
|
||
| const standardEntity = { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as above |
||
| ...entity, | ||
| memberOf: { id: entity.memberOf || '', name: '' }, | ||
| rootCollection: { id: entity.rootCollection || '', name: '' }, | ||
| }; | ||
| const authorisedEntity = await accessTransformer(standardEntity, { request, fastify }); | ||
| if (!authorisedEntity.access.metadata) | ||
| return reply.code(403).send(createForbiddenError('Access to this resource is restricted')); | ||
|
|
||
| const result = await roCrateHandler.get(entity, { request, fastify }); | ||
|
|
||
| if (!result) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -318,3 +318,64 @@ describe('Entities Route', () => { | |
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Entities Route with License Filtering', () => { | ||
| let hasLicense = true; | ||
| async function resolveValidLicenses() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type this so we don't need the ts-expect-error below
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The @ts-expect-error is required to test if the implementation of resolveValidLicenses function returns undefined or null, see below.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defining the function as async function resolveValidLicenses() {
if (hasLicense) {
return ['https://creativecommons.org/licenses/by/4.0/'];
}
return [];
}Should remove the need for the error comment
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would fail the coverage test (branch section) |
||
| if (hasLicense) { | ||
| return ['https://creativecommons.org/licenses/by/4.0/']; | ||
| } | ||
| } | ||
| beforeEach(async () => { | ||
| await fastifyBefore(); | ||
| await fastify.register(entitiesRoute, { | ||
| prisma, | ||
| accessTransformer: AllPublicAccessTransformer, | ||
| // @ts-expect-error | ||
| resolveValidLicenses, | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await fastifyAfter(); | ||
| }); | ||
|
|
||
| describe('GET /entities', () => { | ||
| it('should filter by metadataLicenseId', async () => { | ||
| prisma.entity.findMany.mockResolvedValue([]); | ||
| prisma.entity.count.mockResolvedValue(0); | ||
| hasLicense = true; | ||
| const response = await fastify.inject({ | ||
| method: 'GET', | ||
| url: '/entities', | ||
| }); | ||
|
|
||
| expect(response.statusCode).toBe(200); | ||
| expect(prisma.entity.findMany).toHaveBeenCalledWith({ | ||
| where: { metadataLicenseId: { in: await resolveValidLicenses() } }, | ||
| include: { file: { select: { id: true } } }, | ||
| orderBy: { id: 'asc' }, | ||
| skip: 0, | ||
| take: 100, | ||
| }); | ||
| }); | ||
| it('should return nothing without any valid license', async () => { | ||
| prisma.entity.findMany.mockResolvedValue([]); | ||
| prisma.entity.count.mockResolvedValue(0); | ||
| hasLicense = false; | ||
| const response = await fastify.inject({ | ||
| method: 'GET', | ||
| url: '/entities', | ||
| }); | ||
|
|
||
| expect(response.statusCode).toBe(200); | ||
| expect(prisma.entity.findMany).toHaveBeenCalledWith({ | ||
| where: { metadataLicenseId: { in: [] } }, | ||
| include: { file: { select: { id: true } } }, | ||
| orderBy: { id: 'asc' }, | ||
| skip: 0, | ||
| take: 100, | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import type { ZodTypeProvider } from 'fastify-type-provider-zod'; | |
| import { z } from 'zod/v4'; | ||
| import type { PrismaClient } from '../generated/prisma/client.js'; | ||
| import { baseEntityTransformer, resolveEntityReferences } from '../transformers/default.js'; | ||
| import type { AccessTransformer, EntityTransformer } from '../types/transformers.js'; | ||
| import type { AccessTransformer, EntityTransformer, TransformerContext } from '../types/transformers.js'; | ||
| import { createInternalError } from '../utils/errors.js'; | ||
|
|
||
| const querySchema = z.object({ | ||
|
|
@@ -27,10 +27,11 @@ type EntitiesRouteOptions = { | |
| prisma: PrismaClient; | ||
| accessTransformer: AccessTransformer; | ||
| entityTransformers?: EntityTransformer[]; | ||
| resolveValidLicenses?: (opt: TransformerContext) => Promise<string[]>; | ||
| }; | ||
|
|
||
| const entities: FastifyPluginAsync<EntitiesRouteOptions> = async (fastify, opts) => { | ||
| const { prisma, accessTransformer, entityTransformers = [] } = opts; | ||
| const { prisma, accessTransformer, entityTransformers = [], resolveValidLicenses } = opts; | ||
| fastify.withTypeProvider<ZodTypeProvider>().get( | ||
| '/entities', | ||
| { | ||
|
|
@@ -54,6 +55,12 @@ const entities: FastifyPluginAsync<EntitiesRouteOptions> = async (fastify, opts) | |
| }; | ||
| } | ||
|
|
||
| if (resolveValidLicenses) { | ||
| where.metadataLicenseId = { | ||
| in: (await resolveValidLicenses({ request, fastify })) || [], | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is just an defensive approach as the library can be used as JS library and it is better to provide a sane dafault.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I'm not convinced. This means that if we are provided with a function that has a bug i.e. it has a logic error and doesn't return anything, then we return unexpected data instead. If you want to be defensive, then throw an error if the function does the wrong thing. It is up to the caller to honour the contract. If they don't, that's a bug in their code that we are hiding from them. |
||
| }; | ||
| } | ||
|
|
||
| const [dbEntities, total] = await Promise.all([ | ||
| prisma.entity.findMany({ | ||
| where, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to use the
refMapapproach here as we do elsewhere?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep it simple, as they are not really used yet, and you need to recompute the result in the resolveEntityReferences function. We only need to check the access there by calling the accessTransformer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then we are passing invalid data into
accessTransformer. What if they wanted to restrict access based on thenameofmemberOf?