Added copy-to and related sharing permissions

This commit is contained in:
Stefan Zermatten
2022-11-03 20:18:59 +02:00
parent 358ae46627
commit 8f56a60fb1
11 changed files with 202 additions and 39 deletions

View File

@@ -1,10 +1,12 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
i
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
assertDocCopyPermission,
assertDocEditPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
@@ -19,7 +21,7 @@ if (Meteor.isClient) {
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 100;
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyLibraryNodeTo = new ValidatedMethod({
name: 'libraryNodes.copyTo',
@@ -35,7 +37,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
timeInterval: 10000,
},
run({ _id, parent }) {
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
@@ -45,11 +47,8 @@ const copyLibraryNodeTo = new ValidatedMethod({
}
const libraryNode = LibraryNodes.findOne(_id);
const parentDoc = fetchDocByRef(parent);
assertDocEditPermission(libraryNode, this.userId);
let randomSrc = DDP.randomStream('copyLibraryNodeTo');
let libraryNodeId = randomSrc.id();
libraryNode._id = libraryNodeId;
assertDocCopyPermission(libraryNode, this.userId);
assertDocEditPermission(parentDoc, this.userId);
let decendants = LibraryNodes.find({
'ancestors.id': _id,
@@ -68,7 +67,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
}
}
const nodes = [libraryNode, decendants];
const nodes = [libraryNode, ...decendants];
const newAncestry = parentDoc.ancestors || [];
newAncestry.push(parent);
@@ -92,9 +91,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
collection: LibraryNodes,
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
});
return libraryNodeId;
},
});
export default duplicateLibraryNode;
export default copyLibraryNodeTo;

View File

@@ -16,7 +16,7 @@ if (Meteor.isClient) {
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 100;
const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 1,
timeInterval: 5000,
},
run({ _id }) {

View File

@@ -1,2 +1,3 @@
import '/imports/api/library/methods/copyLibraryNodeTo.js';
import '/imports/api/library/methods/duplicateLibraryNode.js';
import '/imports/api/library/methods/updateReferenceNode.js';

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false,
index: 1,
},
readersCanCopy: {
type: Boolean,
optional: true,
},
});
export default SharingSchema;

View File

@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
},
});
const setReadersCanCopy = new ValidatedMethod({
name: 'sharing.setReadersCanCopy',
validate: new SimpleSchema({
docRef: RefSchema,
readersCanCopy: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ docRef, readersCanCopy }) {
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
return getCollectionByName(docRef.collection).update(docRef.id, {
$set: { readersCanCopy },
});
},
});
const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.updateUserSharePermissions',
validate: new SimpleSchema({
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
},
});
export { setPublic, updateUserSharePermissions, transferOwnership };
export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };

View File

@@ -1,24 +1,25 @@
import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
function assertIdValid(userId) {
if (!userId || typeof userId !== 'string') {
throw new Meteor.Error('Permission denied',
'No user ID. Are you logged in?');
}
}
function assertdocExists(doc){
if (!doc){
function assertdocExists(doc) {
if (!doc) {
throw new Meteor.Error('Permission denied',
'Permission denied: No such document exists');
}
}
export function assertOwnership(doc, userId){
export function assertOwnership(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId ){
if (doc.owner === userId) {
return true;
} else {
throw new Meteor.Error('Permission denied',
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc);
const user = Meteor.users.findOne(userId, {
fields: {
'services.patreon': 1,
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
){
) {
return true;
} else {
throw new Meteor.Error('Edit permission denied',
@@ -59,9 +59,46 @@ export function assertEditPermission(doc, userId) {
}
}
function getRoot(doc){
/**
* Assert that the user can edit the root document which manages its own sharing
* permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertCopyPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')) {
return true;
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
) {
return true;
} else if (
(_.contains(doc.readers, userId) || doc.public) &&
doc.readersCanCopy
) {
return true;
} else {
throw new Meteor.Error('Copy permission denied',
'You do not have permission to copy this document');
}
}
function getRoot(doc) {
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
return fetchDocByRef(doc.ancestors[0]);
} else {
return doc;
@@ -74,11 +111,22 @@ function getRoot(doc){
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocEditPermission(doc, userId){
export function assertDocEditPermission(doc, userId) {
let root = getRoot(doc);
assertEditPermission(root, userId);
}
/**
* Assert that the user can copy a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocCopyPermission(doc, userId) {
let root = getRoot(doc);
assertCopyPermission(root, userId);
}
export function assertViewPermission(doc, userId) {
assertdocExists(doc);
if (doc.public) return true;
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
doc.owner === userId ||
_.contains(doc.readers, userId) ||
_.contains(doc.writers, userId)
){
) {
return true;
} else {
// Admin override
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocViewPermission(doc, userId){
export function assertDocViewPermission(doc, userId) {
let root = getRoot(doc);
assertViewPermission(root, userId);
}
export function assertAdmin(userId){
export function assertAdmin(userId) {
assertIdValid(userId);
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
if (!user){
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
if (!user) {
throw new Meteor.Error('Permission denied',
'UserId does not match any existing user');
}
let isAdmin = user.roles && user.roles.includes('admin')
if (!isAdmin){
if (!isAdmin) {
throw new Meteor.Error('Permission denied',
'User does not have the admin role');
}