From afc40ac923eac57f15b35242500fe925d8635c92 Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 11 Nov 2024 15:33:19 +0100 Subject: [PATCH 1/8] add endpoint to delete documentRoots --- src/controllers/documentRoots.ts | 23 ++++++++++++++++++++- src/models/DocumentRoot.ts | 34 ++++++++++++++++++++++++++++++-- src/routes/router.ts | 4 +++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/controllers/documentRoots.ts b/src/controllers/documentRoots.ts index 426da9a..a57ca89 100644 --- a/src/controllers/documentRoots.ts +++ b/src/controllers/documentRoots.ts @@ -1,10 +1,10 @@ import { RequestHandler } from 'express'; import DocumentRoot, { Config as CreateConfig, UpdateConfig } from '../models/DocumentRoot'; import { ChangedRecord, IoEvent, RecordType } from '../routes/socketEventTypes'; -import { Access } from '@prisma/client'; import { IoRoom } from '../routes/socketEvents'; import { HTTP400Error, HTTP403Error } from '../utils/errors/Errors'; import Document from '../models/Document'; +import { RO_RW_DocumentRootAccess } from '../helpers/accessPolicy'; export const find: RequestHandler<{ id: string }> = async (req, res, next) => { try { @@ -120,3 +120,24 @@ export const permissions: RequestHandler<{ id: string }> = async (req, res, next next(error); } }; + +export const destroy: RequestHandler<{ id: string }> = async (req, res, next) => { + try { + const model = await DocumentRoot.deleteModel(req.user!, req.params.id); + + res.notifications = [ + { + event: IoEvent.DELETED_RECORD, + message: { type: RecordType.DocumentRoot, id: model.id }, + to: [ + ...model.rootGroupPermissions.map((p) => p.studentGroupId), + ...model.rootUserPermissions.map((u) => u.userId), + RO_RW_DocumentRootAccess.has(model.sharedAccess) ? IoRoom.ALL : IoRoom.ADMIN + ] + } + ]; + res.json(model); + } catch (error) { + next(error); + } +}; diff --git a/src/models/DocumentRoot.ts b/src/models/DocumentRoot.ts index e41f790..5fef88b 100644 --- a/src/models/DocumentRoot.ts +++ b/src/models/DocumentRoot.ts @@ -8,10 +8,10 @@ import { RootUserPermission, User } from '@prisma/client'; -import { ApiDocument, prepareDocument } from './Document'; +import { ApiDocument } from './Document'; import { ApiUserPermission } from './RootUserPermission'; import { ApiGroupPermission } from './RootGroupPermission'; -import { HTTP403Error } from '../utils/errors/Errors'; +import { HTTP403Error, HTTP404Error } from '../utils/errors/Errors'; import { asDocumentRootAccess, asGroupAccess, asUserAccess } from '../helpers/accessPolicy'; export type ApiDocumentRoot = DbDocumentRoot & { @@ -215,6 +215,36 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { userPermissions: userPermissions.map(prepareUserPermission), groupPermissions: groupPermissions.map(prepareGroupPermission) }; + }, + async deleteModel(actor: User, id: string) { + const record = await this.findModel(actor, id); + if (!record) { + throw new HTTP404Error('DocumentRoot not found'); + } + if (!actor.isAdmin) { + throw new HTTP403Error('Not authorized'); + } + + const model = await db.delete({ + where: { + id: id + }, + include: { + rootGroupPermissions: { + select: { + access: true, + studentGroupId: true + } + }, + rootUserPermissions: { + select: { + access: true, + userId: true + } + } + } + }); + return model; } }); } diff --git a/src/routes/router.ts b/src/routes/router.ts index 95d05b7..e4630e1 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -32,7 +32,8 @@ import { update as updateDocumentRoot, permissions as allPermissions, findManyFor as findManyDocumentRootsFor, - allDocuments + allDocuments, + destroy as deleteDocumentRoot } from '../controllers/documentRoots'; // initialize router @@ -73,6 +74,7 @@ router.get('/documentRoots', findManyDocumentRoots); router.get('/documentRoots/:id', findDocumentRoot); router.post('/documentRoots/:id', createDocumentRoot); router.put('/documentRoots/:id', updateDocumentRoot); +router.delete('/documentRoots/:id', deleteDocumentRoot); router.get('/documentRoots/:id/permissions', allPermissions); /** * TODO: Reactivate once the controller's permissions are updated. From 379fbd3749864ac495ef802ff059ded4ba5da2f9 Mon Sep 17 00:00:00 2001 From: LeBalz Date: Tue, 12 Nov 2024 22:22:19 +0100 Subject: [PATCH 2/8] add notifications on doc (root) modification --- src/controllers/documentRoots.ts | 24 +++++++++++++++++++++++- src/controllers/documents.ts | 6 ++++-- src/models/DocumentRoot.ts | 20 ++++++++++++++++---- src/routes/socketEventTypes.ts | 4 ++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/controllers/documentRoots.ts b/src/controllers/documentRoots.ts index a57ca89..a2ca553 100644 --- a/src/controllers/documentRoots.ts +++ b/src/controllers/documentRoots.ts @@ -4,7 +4,7 @@ import { ChangedRecord, IoEvent, RecordType } from '../routes/socketEventTypes'; import { IoRoom } from '../routes/socketEvents'; import { HTTP400Error, HTTP403Error } from '../utils/errors/Errors'; import Document from '../models/Document'; -import { RO_RW_DocumentRootAccess } from '../helpers/accessPolicy'; +import { NoneAccess, RO_RW_DocumentRootAccess } from '../helpers/accessPolicy'; export const find: RequestHandler<{ id: string }> = async (req, res, next) => { try { @@ -80,6 +80,28 @@ export const create: RequestHandler<{ id: string }, any, CreateConfig | undefine ) => { try { const documentRoot = await DocumentRoot.createModel(req.params.id, req.body); + /** + * Notifications to + * - the user who created the document + * - users with ro/rw access to the document root + * - student groups with ro/rw access to the document root + */ + const groupIds = documentRoot.groupPermissions + .filter((p) => !NoneAccess.has(p.access)) + .map((p) => p.groupId); + const userIds = documentRoot.userPermissions + .filter((p) => !NoneAccess.has(p.access)) + .map((p) => p.userId); + const sharedAccess = RO_RW_DocumentRootAccess.has(documentRoot.sharedAccess) + ? IoRoom.ALL + : IoRoom.ADMIN; + res.notifications = [ + { + event: IoEvent.NEW_RECORD, + message: { type: RecordType.DocumentRoot, record: documentRoot }, + to: [...groupIds, ...userIds, sharedAccess, req.user!.id] // overlappings are handled by socket.io: https://socket.io/docs/v3/rooms/#joining-and-leaving + } + ]; res.json(documentRoot); } catch (error) { next(error); diff --git a/src/controllers/documents.ts b/src/controllers/documents.ts index a5bef6a..ce9bbbb 100644 --- a/src/controllers/documents.ts +++ b/src/controllers/documents.ts @@ -33,12 +33,14 @@ export const create: RequestHandler = async (req, res, nex */ const groupIds = permissions.group.filter((p) => !NoneAccess.has(p.access)).map((p) => p.groupId); const userIds = permissions.user.filter((p) => !NoneAccess.has(p.access)).map((p) => p.userId); - const sharedAccess = RO_RW_DocumentRootAccess.has(permissions.sharedAccess) ? [IoRoom.ALL] : []; + const sharedAccess = RO_RW_DocumentRootAccess.has(permissions.sharedAccess) + ? IoRoom.ALL + : IoRoom.ADMIN; res.notifications = [ { event: IoEvent.NEW_RECORD, message: { type: RecordType.Document, record: model }, - to: [...groupIds, ...userIds, ...sharedAccess, IoRoom.ADMIN, req.user!.id] // overlappings are handled by socket.io: https://socket.io/docs/v3/rooms/#joining-and-leaving + to: [...groupIds, ...userIds, sharedAccess, req.user!.id] // overlappings are handled by socket.io: https://socket.io/docs/v3/rooms/#joining-and-leaving, } ]; res.status(200).json(model); diff --git a/src/models/DocumentRoot.ts b/src/models/DocumentRoot.ts index 5fef88b..ce15d74 100644 --- a/src/models/DocumentRoot.ts +++ b/src/models/DocumentRoot.ts @@ -26,7 +26,7 @@ type Permissions = { groupPermissions: ApiGroupPermission[]; }; -export type ApiDocumentRootUpdate = DbDocumentRoot & Permissions; +export type ApiDocumentRootWithoutDocuments = Omit; export type AccessCheckableDocumentRoot = DbDocumentRoot & { rootGroupPermissions: RootGroupPermission[]; @@ -143,8 +143,8 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { } return response; }, - async createModel(id: string, config: Config = {}): Promise { - return db.create({ + async createModel(id: string, config: Config = {}): Promise { + const model = await db.create({ data: { id: id, access: asDocumentRootAccess(config.access), @@ -170,10 +170,22 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { } } : undefined + }, + include: { + rootGroupPermissions: true, + rootUserPermissions: true } }); + + return { + id: model.id, + access: model.access, + sharedAccess: model.sharedAccess, + userPermissions: model.rootUserPermissions.map((p) => prepareUserPermission(p)), + groupPermissions: model.rootGroupPermissions.map((p) => prepareGroupPermission(p)) + }; }, - async updateModel(id: string, data: UpdateConfig): Promise { + async updateModel(id: string, data: UpdateConfig): Promise { const model = await db.update({ where: { id: id diff --git a/src/routes/socketEventTypes.ts b/src/routes/socketEventTypes.ts index 8ddc4b8..f255238 100644 --- a/src/routes/socketEventTypes.ts +++ b/src/routes/socketEventTypes.ts @@ -2,7 +2,7 @@ import { Prisma, User } from '@prisma/client'; import { ApiDocument } from '../models/Document'; import { ApiUserPermission } from '../models/RootUserPermission'; import { ApiGroupPermission } from '../models/RootGroupPermission'; -import { ApiDocumentRootUpdate } from '../models/DocumentRoot'; +import { ApiDocumentRootWithoutDocuments } from '../models/DocumentRoot'; export enum IoEvent { NEW_RECORD = 'NEW_RECORD', @@ -25,7 +25,7 @@ type TypeRecordMap = { [RecordType.User]: User; [RecordType.UserPermission]: ApiUserPermission; [RecordType.GroupPermission]: ApiGroupPermission; - [RecordType.DocumentRoot]: ApiDocumentRootUpdate; + [RecordType.DocumentRoot]: ApiDocumentRootWithoutDocuments; }; export interface NewRecord { From 7e6f092333817bdab607450ecf7d12b5e5927aaa Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 18 Nov 2024 11:21:59 +0100 Subject: [PATCH 3/8] don't grant admins full access to all routes --- src/auth/guard.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/auth/guard.ts b/src/auth/guard.ts index e1807a8..37ddbcd 100644 --- a/src/auth/guard.ts +++ b/src/auth/guard.ts @@ -100,9 +100,6 @@ const requestHasRequiredAttributes = ( method: string, isAdmin: boolean ) => { - if (isAdmin) { - return true; - } const accessRules = Object.values(accessMatrix); const accessRule = accessRules .filter((accessRule) => accessRule.regex.test(path)) @@ -111,9 +108,11 @@ const requestHasRequiredAttributes = ( if (!accessRule) { return false; } - return accessRule.access.some( - (rule) => !rule.adminOnly && rule.methods.includes(method as 'GET' | 'POST' | 'PUT' | 'DELETE') + const hasAccess = accessRule.access.some( + (rule) => (isAdmin || !rule.adminOnly) && rule.methods.includes(method as 'GET' | 'POST' | 'PUT' | 'DELETE') ); + Logger.info(`${hasAccess ? '✅' : '❌'} Access Rule for ${isAdmin ? 'Admin' : 'User'}: [${method}:${path}] ${JSON.stringify(accessRule)}`); + return hasAccess; }; export default routeGuard; From e1a9b7d07c7241514dbcc953af1e6cf0fdf8dad9 Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 18 Nov 2024 11:38:37 +0100 Subject: [PATCH 4/8] add 'DELETE' to docRoots for admins --- src/routes/authConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/authConfig.ts b/src/routes/authConfig.ts index 11672b6..4b34274 100644 --- a/src/routes/authConfig.ts +++ b/src/routes/authConfig.ts @@ -135,7 +135,7 @@ const authConfig: Config = { adminOnly: false }, { - methods: ['PUT'], + methods: ['PUT', 'DELETE'], adminOnly: true } ] From 6689c0514346baed97f3566ddc93a6c1322c5d14 Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 18 Nov 2024 11:42:14 +0100 Subject: [PATCH 5/8] include permissions on missed doc roots --- src/models/Document.ts | 34 +++++++++++++++++++++--- src/models/DocumentRoot.ts | 53 +++++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/models/Document.ts b/src/models/Document.ts index 80d64bc..e31bd4e 100644 --- a/src/models/Document.ts +++ b/src/models/Document.ts @@ -6,6 +6,7 @@ import DocumentRoot, { AccessCheckableDocumentRoot } from './DocumentRoot'; import { highestAccess, NoneAccess, RWAccess } from '../helpers/accessPolicy'; import { ApiGroupPermission } from './RootGroupPermission'; import { ApiUserPermission } from './RootUserPermission'; +import Logger from '../utils/logger'; type AccessCheckableDocument = DbDocument & { documentRoot: AccessCheckableDocumentRoot; @@ -19,7 +20,9 @@ interface DocumentWithPermission { } const extractPermission = (actor: User, document: AccessCheckableDocument): Access | null => { - if (NoneAccess.has(document.documentRoot.sharedAccess) && document.authorId !== actor.id) { + const hasBaseAccess = + document.authorId === actor.id || !NoneAccess.has(document.documentRoot.sharedAccess); + if (!hasBaseAccess) { return null; } @@ -106,6 +109,10 @@ function Document(db: PrismaClient['document']) { if (!documentRoot) { throw new HTTP404Error('Document root not found'); } + /** + * Since it is easyier to check wheter a user has permissions to create a model + * when the model actually exists, we create the model first and then check the permissions. + */ if (parentId) { const parent = await this.findModel(actor, parentId); if (!parent) { @@ -157,6 +164,20 @@ function Document(db: PrismaClient['document']) { } }) .then((doc) => prepareDocument(actor, doc)!); + /** + * Check if the user has the required permissions to create the model. + * If not, delete the model and throw an error. + */ + const canCreate = RWAccess.has(model.highestPermission); + if (!canCreate) { + Logger.info(`❌ New Model [${model.document.id}]: ${model.highestPermission}`); + db.delete({ + where: { + id: model.document.id + } + }); + throw new HTTP403Error('Insufficient access permission'); + } return { model: model.document, permissions: { @@ -173,7 +194,10 @@ function Document(db: PrismaClient['document']) { if (!record) { throw new HTTP404Error('Document not found'); } - const canWrite = record.document.authorId === actor.id || RWAccess.has(record.highestPermission); + /** + * models can be updated when the user has RW access + */ + const canWrite = RWAccess.has(record.highestPermission); if (!canWrite) { throw new HTTP403Error('Not authorized'); } @@ -214,7 +238,11 @@ function Document(db: PrismaClient['document']) { if (!record) { throw new HTTP404Error('Document not found'); } - if (!(record.document.authorId === actor.id && RWAccess.has(record.highestPermission))) { + /** + * models can be deleted when the actor is the author and has RW access. + */ + const canDelete = record.document.authorId === actor.id && RWAccess.has(record.highestPermission); + if (!canDelete) { throw new HTTP403Error('Not authorized'); } diff --git a/src/models/DocumentRoot.ts b/src/models/DocumentRoot.ts index ce15d74..66e3513 100644 --- a/src/models/DocumentRoot.ts +++ b/src/models/DocumentRoot.ts @@ -77,9 +77,31 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { } })) as ApiDocumentRoot | null; if (!documentRoot) { + /** + * The user does not have documents in the requested document root (and thus no docRoot is returned from the view). + * In this case, we have to load the document root directly. + */ const docRoot = await db.findUnique({ where: { id: id + }, + include: { + rootUserPermissions: { + where: { + userId: actor.id + } + }, + rootGroupPermissions: { + where: { + studentGroup: { + users: { + some: { + id: actor.id + } + } + } + } + } } }); if (!docRoot) { @@ -88,13 +110,11 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { return { ...docRoot, documents: [], - userPermissions: [], - groupPermissions: [] + userPermissions: docRoot.rootUserPermissions.map((p) => prepareUserPermission(p)), + groupPermissions: docRoot.rootGroupPermissions.map((p) => prepareGroupPermission(p)) }; } - if (documentRoot) { - delete (documentRoot as any).userId; - } + delete (documentRoot as any).userId; return documentRoot; }, async findManyModels( @@ -130,20 +150,39 @@ function DocumentRoot(db: PrismaClient['documentRoot']) { id: { in: missingDocumentRoots } + }, + include: { + rootUserPermissions: { + where: { + userId: actorId + } + }, + rootGroupPermissions: { + where: { + studentGroup: { + users: { + some: { + id: actorId + } + } + } + } + } } }); response.push( ...docRoots.map((docRoot) => ({ ...docRoot, documents: [], - userPermissions: [], - groupPermissions: [] + userPermissions: docRoot.rootUserPermissions.map((p) => prepareUserPermission(p)), + groupPermissions: docRoot.rootGroupPermissions.map((p) => prepareGroupPermission(p)) })) ); } return response; }, async createModel(id: string, config: Config = {}): Promise { + // TODO: assign a RW-Access for the admin group for each created doc-root!? const model = await db.create({ data: { id: id, From c700a930648054d16c443dc25b92360a40a2524b Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 18 Nov 2024 11:42:34 +0100 Subject: [PATCH 6/8] add some dev reporting for the access rules --- src/auth/guard.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/auth/guard.ts b/src/auth/guard.ts index 37ddbcd..7578127 100644 --- a/src/auth/guard.ts +++ b/src/auth/guard.ts @@ -109,9 +109,12 @@ const requestHasRequiredAttributes = ( return false; } const hasAccess = accessRule.access.some( - (rule) => (isAdmin || !rule.adminOnly) && rule.methods.includes(method as 'GET' | 'POST' | 'PUT' | 'DELETE') + (rule) => + (isAdmin || !rule.adminOnly) && rule.methods.includes(method as 'GET' | 'POST' | 'PUT' | 'DELETE') + ); + Logger.info( + `${hasAccess ? '✅' : '❌'} Access Rule for ${isAdmin ? 'Admin' : 'User'}: [${method}:${path}] ${JSON.stringify(accessRule)}` ); - Logger.info(`${hasAccess ? '✅' : '❌'} Access Rule for ${isAdmin ? 'Admin' : 'User'}: [${method}:${path}] ${JSON.stringify(accessRule)}`); return hasAccess; }; From 39d3a19f475b3a24f17dcd5a17c2ca396091bbf3 Mon Sep 17 00:00:00 2001 From: LeBalz Date: Mon, 18 Nov 2024 19:51:12 +0100 Subject: [PATCH 7/8] fix wrong access bug --- .../migration.sql | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 prisma/migrations/20241118183039_fix_shared_access_in_view__all_document_user_permissions/migration.sql diff --git a/prisma/migrations/20241118183039_fix_shared_access_in_view__all_document_user_permissions/migration.sql b/prisma/migrations/20241118183039_fix_shared_access_in_view__all_document_user_permissions/migration.sql new file mode 100644 index 0000000..bc3e2e8 --- /dev/null +++ b/prisma/migrations/20241118183039_fix_shared_access_in_view__all_document_user_permissions/migration.sql @@ -0,0 +1,181 @@ + +CREATE OR REPLACE VIEW view__all_document_user_permissions AS + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id, + ROW_NUMBER() OVER (PARTITION BY document_root_id, user_id, document_id ORDER BY access DESC) AS access_rank + FROM ( + -- get all documents where the user **is the author** + -- including all child documents + WITH RECURSIVE + document_hierarchy AS ( + -- Anchor member: select the root document + SELECT + document_roots.id AS document_root_id, + documents.id AS document_id, + documents.author_id AS user_id, + document_roots.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + INNER JOIN documents ON document_roots.id = documents.document_root_id + WHERE documents.parent_id IS NULL -- Assuming root documents have parent_id as NULL + + UNION ALL -- keeps duplicates in the result set + + -- Recursive member: select child documents with the parent's author as the user + SELECT + document_hierarchy.document_root_id AS document_root_id, + child_documents.id AS document_id, + document_hierarchy.user_id AS user_id, + document_hierarchy.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_hierarchy + INNER JOIN documents AS child_documents ON document_hierarchy.document_id = child_documents.parent_id + ), + -- get all documents where the user is **not the author** + -- but has been granted **shared access** + shared_doc_hierarchy AS ( + -- Anchor member: select the root document + SELECT + document_roots.id AS document_root_id, + documents.id AS document_id, + all_users.id AS user_id, + CASE + WHEN document_roots.shared_access <= document_roots.access THEN document_roots.shared_access + ELSE document_roots.access + END AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + INNER JOIN documents ON document_roots.id = documents.document_root_id + CROSS JOIN users all_users + WHERE documents.parent_id IS NULL -- Assuming root documents have parent_id as NULL + AND documents.author_id != all_users.id + AND document_roots.access != 'None_DocumentRoot' + AND ( + document_roots.shared_access='RO_DocumentRoot' + OR + document_roots.shared_access='RW_DocumentRoot' + ) + + UNION ALL -- keeps duplicates in the result set + + -- Recursive member: select child documents with the parent's author as the user + SELECT + shared_doc_hierarchy.document_root_id AS document_root_id, + child_documents.id AS document_id, + shared_doc_hierarchy.user_id AS user_id, + shared_doc_hierarchy.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + shared_doc_hierarchy + INNER JOIN documents AS child_documents ON shared_doc_hierarchy.document_id = child_documents.parent_id + ) + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id + FROM document_hierarchy + UNION ALL + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id + FROM shared_doc_hierarchy + UNION ALL + -- get all documents where the user has been granted shared access + -- or the access has been extended by user permissions + SELECT + document_roots.id AS document_root_id, + rup.user_id AS user_id, + rup.access AS access, + documents.id AS document_id, + rup.id AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN root_user_permissions rup + ON ( + document_roots.id = rup.document_root_id + AND ( + documents.author_id = rup.user_id + OR + rup.access >= document_roots.shared_access + ) + ) + WHERE rup.user_id IS NOT NULL + UNION ALL + -- all group-based permissions for the documents author + SELECT + document_roots.id AS document_root_id, + sg_to_user."B" AS user_id, + rgp.access AS access, + documents.id AS document_id, + NULL::uuid AS root_user_permission_id, + rgp.id AS root_group_permission_id, + sg.id AS group_id + FROM + document_roots + INNER JOIN root_group_permissions rgp ON document_roots.id=rgp.document_root_id + INNER JOIN student_groups sg ON rgp.student_group_id=sg.id + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN "_StudentGroupToUser" sg_to_user + ON ( + sg_to_user."A"=sg.id + AND ( + sg_to_user."B"=documents.author_id + OR documents.author_id is null + ) + ) + WHERE sg_to_user."B" IS NOT NULL + UNION ALL + -- all group based permissions for the user, which is not the author + SELECT + document_roots.id AS document_root_id, + sg_to_user."B" AS user_id, + rgp.access AS access, + documents.id AS document_id, + NULL::uuid AS root_user_permission_id, + rgp.id AS root_group_permission_id, + sg.id AS group_id + FROM + document_roots + INNER JOIN root_group_permissions rgp + ON ( + document_roots.id=rgp.document_root_id + AND rgp.access >= document_roots.shared_access + ) + INNER JOIN student_groups sg ON rgp.student_group_id=sg.id + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN "_StudentGroupToUser" sg_to_user + ON ( + sg_to_user."A"=sg.id + AND sg_to_user."B"!=documents.author_id + ) + WHERE sg_to_user."B" IS NOT NULL + ) as doc_user_permissions; From 7d71450562dbf528fa863e8a70162b94432dc45f Mon Sep 17 00:00:00 2001 From: LeBalz Date: Tue, 19 Nov 2024 12:10:43 +0100 Subject: [PATCH 8/8] =?UTF-8?q?fix=20shared=20permissions,=20again=20?= =?UTF-8?q?=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 prisma/migrations/20241119111720_fix_shared_access_in_view__all_document_user_permissions/migration.sql diff --git a/prisma/migrations/20241119111720_fix_shared_access_in_view__all_document_user_permissions/migration.sql b/prisma/migrations/20241119111720_fix_shared_access_in_view__all_document_user_permissions/migration.sql new file mode 100644 index 0000000..c3b0cd5 --- /dev/null +++ b/prisma/migrations/20241119111720_fix_shared_access_in_view__all_document_user_permissions/migration.sql @@ -0,0 +1,190 @@ + +CREATE OR REPLACE VIEW view__all_document_user_permissions AS + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id, + ROW_NUMBER() OVER (PARTITION BY document_root_id, user_id, document_id ORDER BY access DESC) AS access_rank + FROM ( + -- get all documents where the user **is the author** + -- including all child documents + WITH RECURSIVE + document_hierarchy AS ( + -- Anchor member: select the root document + SELECT + document_roots.id AS document_root_id, + documents.id AS document_id, + documents.author_id AS user_id, + document_roots.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + INNER JOIN documents ON document_roots.id = documents.document_root_id + WHERE documents.parent_id IS NULL -- Assuming root documents have parent_id as NULL + + UNION ALL -- keeps duplicates in the result set + + -- Recursive member: select child documents with the parent's author as the user + SELECT + document_hierarchy.document_root_id AS document_root_id, + child_documents.id AS document_id, + document_hierarchy.user_id AS user_id, + document_hierarchy.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_hierarchy + INNER JOIN documents AS child_documents ON document_hierarchy.document_id = child_documents.parent_id + ), + -- get all documents where the user is **not the author** + -- but has been granted **shared access** + shared_doc_hierarchy AS ( + -- Anchor member: select the root document + SELECT + document_roots.id AS document_root_id, + documents.id AS document_id, + all_users.id AS user_id, + CASE + WHEN document_roots.shared_access <= document_roots.access THEN document_roots.shared_access + ELSE document_roots.access + END AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + INNER JOIN documents ON document_roots.id = documents.document_root_id + CROSS JOIN users all_users + WHERE documents.parent_id IS NULL -- Assuming root documents have parent_id as NULL + AND documents.author_id != all_users.id + AND document_roots.access != 'None_DocumentRoot' + AND document_roots.shared_access != 'None_DocumentRoot' + + UNION ALL -- keeps duplicates in the result set + + -- Recursive member: select child documents with the parent's author as the user + SELECT + shared_doc_hierarchy.document_root_id AS document_root_id, + child_documents.id AS document_id, + shared_doc_hierarchy.user_id AS user_id, + shared_doc_hierarchy.access AS access, + NULL::uuid AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + shared_doc_hierarchy + INNER JOIN documents AS child_documents ON shared_doc_hierarchy.document_id = child_documents.parent_id + ) + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id + FROM document_hierarchy + UNION ALL + SELECT + document_root_id, + user_id, + access, + document_id, + root_user_permission_id, + root_group_permission_id, + group_id + FROM shared_doc_hierarchy + UNION ALL + -- get all documents where the user has been granted shared access + -- or the access has been extended by user permissions + SELECT + document_roots.id AS document_root_id, + rup.user_id AS user_id, + CASE + WHEN documents.author_id = rup.user_id THEN rup.access + WHEN document_roots.shared_access = 'RO_DocumentRoot' THEN 'RO_User' + WHEN document_roots.shared_access = 'RW_DocumentRoot' THEN 'RW_User' + ELSE document_roots.shared_access + END AS access, + documents.id AS document_id, + rup.id AS root_user_permission_id, + NULL::uuid AS root_group_permission_id, + NULL::uuid AS group_id + FROM + document_roots + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN root_user_permissions rup + ON ( + document_roots.id = rup.document_root_id + AND ( + documents.author_id = rup.user_id + OR -- shared access + ( + rup.access >= document_roots.shared_access + AND document_roots.shared_access != 'None_DocumentRoot' + ) + ) + ) + WHERE rup.user_id IS NOT NULL + UNION ALL + -- all group-based permissions for the documents author + SELECT + document_roots.id AS document_root_id, + sg_to_user."B" AS user_id, + rgp.access AS access, + documents.id AS document_id, + NULL::uuid AS root_user_permission_id, + rgp.id AS root_group_permission_id, + sg.id AS group_id + FROM + document_roots + INNER JOIN root_group_permissions rgp ON document_roots.id=rgp.document_root_id + INNER JOIN student_groups sg ON rgp.student_group_id=sg.id + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN "_StudentGroupToUser" sg_to_user + ON ( + sg_to_user."A"=sg.id + AND ( + sg_to_user."B"=documents.author_id + OR documents.author_id is null + ) + ) + WHERE sg_to_user."B" IS NOT NULL + UNION ALL + -- all group based permissions for the user, which is not the author + SELECT + document_roots.id AS document_root_id, + sg_to_user."B" AS user_id, + CASE + WHEN document_roots.shared_access = 'RO_DocumentRoot' THEN 'RO_StudentGroup' + WHEN document_roots.shared_access = 'RW_DocumentRoot' THEN 'RW_StudentGroup' + ELSE document_roots.shared_access + END AS access, + documents.id AS document_id, + NULL::uuid AS root_user_permission_id, + rgp.id AS root_group_permission_id, + sg.id AS group_id + FROM + document_roots + INNER JOIN root_group_permissions rgp + ON ( + document_roots.id=rgp.document_root_id + AND rgp.access >= document_roots.shared_access + AND document_roots.shared_access != 'None_DocumentRoot' + ) + INNER JOIN student_groups sg ON rgp.student_group_id=sg.id + LEFT JOIN documents ON document_roots.id=documents.document_root_id + LEFT JOIN "_StudentGroupToUser" sg_to_user + ON ( + sg_to_user."A"=sg.id + AND sg_to_user."B"!=documents.author_id + ) + WHERE sg_to_user."B" IS NOT NULL + ) as doc_user_permissions;