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
57 changes: 10 additions & 47 deletions src/controllers/documentRoots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,6 @@ import { HTTP400Error, HTTP403Error } from '../utils/errors/Errors.js';
import Document from '../models/Document.js';
import { NoneAccess, RO_RW_DocumentRootAccess } from '../helpers/accessPolicy.js';
import { hasElevatedAccess } from '../models/User.js';
import { Access } from '../../prisma/generated/enums.js';

export const find: RequestHandler<{ id: string }> = async (req, res, next) => {
const document = await DocumentRoot.findModel((req as any).user!, req.params.id);
res.json(document);
};

export const findMany: RequestHandler<any, any, any, { ids: string[] }> = async (req, res, next) => {
const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
if (ids.length === 0 || !req.query.ids) {
return res.json([]);
}
const documents = await DocumentRoot.findManyModels((req as any).user!.id, ids);
res.json(documents);
};

export const findMultipleFor: RequestHandler<
{ id: string /** userId */ },
Expand All @@ -45,35 +30,26 @@ export const findMultipleFor: RequestHandler<
res.json(documents);
};

export const allDocuments: RequestHandler<any, any, any, { rids: string[] }> = async (req, res, next) => {
if (!hasElevatedAccess((req as any).user!.role)) {
throw new HTTP403Error('Not Authorized');
}
const ids = Array.isArray(req.query.rids) ? req.query.rids : [req.query.rids];
if (ids.length === 0) {
return res.json([]);
}
const documents = await Document.allOfDocumentRoots((req as any).user!, ids);
res.json(documents);
};

export const multipleDocuments: RequestHandler<
any,
any,
{ documentRootIds: string[]; userId?: string }
> = async (req, res, next) => {
const user = req.user;
if (!hasElevatedAccess(user.role)) {
throw new HTTP403Error('Not Authorized');
}
const ids = req.body.documentRootIds;
if (ids.length === 0) {
return res.json([]);
}
const documents = await Document.allOfDocumentRoots(
{ role: user.role, id: req.body.userId ?? user.id },
ids
);
if (!hasElevatedAccess(user.role)) {
if (req.body.userId && req.body.userId !== user.id) {
throw new HTTP403Error('Not authorized');
}
const documents = await DocumentRoot.findManyModels(user.id, ids, {
ignoreMissingRoots: false
});
return res.json(documents?.flatMap((dr) => dr.documents ?? []) ?? []);
}
const documents = await Document.allOfDocumentRoots(user, ids, req.body.userId);
res.json(documents);
};

Expand Down Expand Up @@ -136,19 +112,6 @@ export const permissions: RequestHandler<any, any, { documentRootIds: string[] }
const permissions = await DocumentRoot.getPermissions((req as any).user!, req.body.documentRootIds);
res.json(permissions);
};
// TODO: remove this endpoint once the permissions [POST]/documentRoots/permissions endpoint is established and clients are updated
export const singlePermissions: RequestHandler<{ id: string }> = async (req, res, next) => {
const permissions = await DocumentRoot.getPermissions((req as any).user!, [req.params.id]);
res.json(
permissions[0] ?? {
id: req.params.id,
access: Access.None_DocumentRoot,
sharedAccess: Access.None_DocumentRoot,
userPermissions: [],
groupPermissions: []
}
);
};

export const destroy: RequestHandler<{ id: string }> = async (req, res, next) => {
const model = await DocumentRoot.deleteModel((req as any).user!, req.params.id);
Expand Down
5 changes: 0 additions & 5 deletions src/controllers/studentGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import StudentGroup from '../models/StudentGroup.js';
import { IoEvent, RecordType } from '../routes/socketEventTypes.js';
import { JsonObject } from '@prisma/client/runtime/client';

export const find: RequestHandler<{ id: string }> = async (req, res, next) => {
const group = await StudentGroup.findModel((req as any).user!, req.params.id);
res.json(group);
};

export const create: RequestHandler<any, any, DbStudentGroup> = async (req, res, next) => {
const { name, description, parentId } = req.body;
const model = await StudentGroup.createModel((req as any).user!, name, description, parentId);
Expand Down
20 changes: 16 additions & 4 deletions src/models/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,20 +265,32 @@ function Document(db: PrismaClient['document']) {
},

async allOfDocumentRoots(
actor: User | { role: Role | string; id: string },
documentRootIds: string[]
actor: User,
documentRootIds: string[],
authorId?: string
): Promise<DbDocument[]> {
if (!hasElevatedAccess(actor.role)) {
throw new HTTP403Error('Not authorized');
}
if (actor.role === Role.ADMIN) {
return db.findMany({ where: { documentRootId: { in: documentRootIds } } });
return db.findMany({
where: { documentRootId: { in: documentRootIds }, authorId: authorId }
});
}
// only include documents where the author is in the same group as the actor.
const documents = await db.findMany({
where: {
documentRootId: { in: documentRootIds },
author: whereStudentGroupAccess(actor.id, true)
author: {
id: authorId,
studentGroups: {
some: {
studentGroup: {
users: { some: { userId: actor.id, isAdmin: true } }
}
}
}
}
}
});
return documents;
Expand Down
3 changes: 3 additions & 0 deletions src/models/DocumentRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ function DocumentRoot(db: PrismaClient['documentRoot']) {
documentType?: string;
} = {}
): Promise<ApiDocumentRoot[] | null> {
if (!actorId) {
throw new HTTP403Error('Not authorized');
}
const documentRoots = (await prisma.view_UsersDocuments.findMany({
where: { id: { in: ids }, userId: actorId },
relationLoadStrategy: 'query'
Expand Down
8 changes: 1 addition & 7 deletions src/routes/authConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface Config {

const authConfig: Config = {
accessMatrix: {
checklogin: { path: '/checklogin', access: [{ methods: ['GET'], minRole: Role.STUDENT }] },
user: { path: '/user', access: [{ methods: ['GET', 'POST'], minRole: Role.STUDENT }] },
admin: {
path: '/admin',
Expand Down Expand Up @@ -42,7 +41,7 @@ const authConfig: Config = {
},
documentsMultiple: {
path: '/documents/multiple',
access: [{ methods: ['POST'], minRole: Role.TEACHER }]
access: [{ methods: ['POST'], minRole: Role.STUDENT }]
},
documentRoots: {
path: '/documentRoots',
Expand All @@ -55,11 +54,6 @@ const authConfig: Config = {
path: '/documentRoots/permissions',
access: [{ methods: ['POST'], minRole: Role.TEACHER }]
},
// TODO: remove this endpoint once the permissions [POST]/documentRoots/permissions endpoint is established and clients are updated
documentRootPermissions: {
path: '/documentRoots/:id/permissions',
access: [{ methods: ['GET'], minRole: Role.TEACHER }]
},
githubToken: { path: '/cms', access: [{ methods: ['GET', 'PUT'], minRole: Role.STUDENT }] },
githubLogout: { path: '/cms/logout', access: [{ methods: ['POST'], minRole: Role.STUDENT }] }
}
Expand Down
31 changes: 5 additions & 26 deletions src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
all as allStudentGroups,
create as createStudentGroup,
destroy as deleteStudentGroup,
find as findStudentGroup,
update as updateStudentGroup,
addUser as addStudentGroupUser,
removeUser as removeStudentGroupUser,
Expand All @@ -23,19 +22,15 @@ import {
import {
create as createDocument,
destroy as deleteDocument,
find as findDocument,
update as updateDocument,
linkTo as linkDocument
linkTo as linkDocument,
find as findDocument
} from '../controllers/documents.js';
import {
create as createDocumentRoot,
find as findDocumentRoot,
findMany as findManyDocumentRoots,
update as updateDocumentRoot,
permissions as allPermissions,
singlePermissions as allPermissionsFor,
findMultipleFor as findMultipleDocumentRootsFor,
allDocuments,
destroy as deleteDocumentRoot,
multipleDocuments
} from '../controllers/documentRoots.js';
Expand Down Expand Up @@ -71,10 +66,6 @@ router.post('/users/:id/documentRoots', findMultipleDocumentRootsFor);

router.get('/studentGroups', allStudentGroups);
router.post('/studentGroups', createStudentGroup);
/**
* TODO: do we need id-based access?
*/
router.get('/studentGroups/:id', findStudentGroup);

router.put('/studentGroups/:id', updateStudentGroup);
router.delete('/studentGroups/:id', deleteStudentGroup);
Expand All @@ -90,29 +81,16 @@ router.post('/permissions/group', createStudentGroupPermission);
router.put('/permissions/group/:id', updateStudentGroupPermission);
router.delete('/permissions/group/:id', deleteStudentGroupPermission);

router.get('/documentRoots', findManyDocumentRoots);
router.get('/documentRoots/:id', findDocumentRoot);
// TODO: remove this endpoint once the permissions [POST]/documentRoots/permissions endpoint is established and clients are updated
router.get('/documentRoots/:id/permissions', allPermissionsFor);
// order matters here! /documentRoots/:id would match /documentRoots/:id/permissions if it was placed before
router.post('/documentRoots/permissions', allPermissions);
router.post('/documentRoots/:id', createDocumentRoot);
router.put('/documentRoots/:id', updateDocumentRoot);
router.delete('/documentRoots/:id', deleteDocumentRoot);

router.post('/documents', createDocument);
/**
* TODO: remove once /documents/multiple is established and clients are updated
*
* @adminOnly --> handle in controller
* Returns all documents which are linked to the **document roots**.
* @requires ?rids: string[] -> the document root ids
*/
router.get('/documents', allDocuments);

/**
* a post endpoint to prevent issues with long query strings when requesting
* many document roots for a user
* @adminOnly --> handle in controller
* many document roots (for the current user, or when having elevated access, for any user)
* Returns all documents which are linked to the **document roots**.
*/
router.post('/documents/multiple', multipleDocuments);
Expand All @@ -132,4 +110,5 @@ router.get('/cms/settings', findCmsSettings);
router.put('/cms/settings', updateCmsSettings);
router.get('/cms/github-token', githubToken);
router.post('/cms/logout', githubLogout);

export default router;
30 changes: 26 additions & 4 deletions src/tests/integration/documentRoots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,40 @@ describe('DocumentRoots (integration)', () => {
expect(createRes.body.id).toBe(documentRootId);
expect(createRes.body.access).toBe(Access.RW_DocumentRoot);

const getRes = await agent.get(`${API_URL}/documentRoots/${documentRootId}`);
const getRes = await agent.post(`${API_URL}/users/${user.id}/documentRoots`).send({
documentRootIds: [documentRootId]
});
expect(getRes.status).toBe(200);
expect(getRes.body.id).toBe(documentRootId);
expect(getRes.body.documents).toEqual([]);
expect(getRes.body.length).toBe(1);
expect(getRes.body[0].id).toBe(documentRootId);
expect(getRes.body[0].documents).toEqual([]);
});

it('rejects unauthenticated requests', async () => {
const user = await createTestUser(Role.STUDENT);
const documentRootId = randomUUID();
const res = await request(app).get(`${API_URL}/documentRoots/${documentRootId}`);

const res = await request(app)
.post(`${API_URL}/users/${user.id}/documentRoots`)
.send({
documentRootIds: [documentRootId]
});
expect(res.status).toBe(401);
});

it('rejects user to fetch others documents', async () => {
const user = await createTestUser(Role.STUDENT);
const otherUser = await createTestUser(Role.STUDENT);
const documentRootId = randomUUID();

const agent = agentAs(user.id);

const res = await agent.post(`${API_URL}/users/${otherUser.id}/documentRoots`).send({
documentRootIds: [documentRootId]
});
expect(res.status).toBe(403);
});

it('only allows an admin to delete a document root', async () => {
const student = await createTestUser(Role.STUDENT);
const admin = await createTestUser(Role.ADMIN);
Expand Down
Loading