Collection Permissions (#829)

see https://github.com/outline/outline/issues/668
This commit is contained in:
Tom Moor
2019-01-05 13:37:33 -08:00
committed by GitHub
parent 8978915423
commit 8c02b0028c
53 changed files with 1379 additions and 214 deletions

View File

@@ -6,6 +6,7 @@ import { DataTypes, sequelize } from '../sequelize';
import { asyncLock } from '../redis';
import events from '../events';
import Document from './Document';
import CollectionUser from './CollectionUser';
import Event from './Event';
import { welcomeMessage } from '../utils/onboarding';
@@ -26,6 +27,7 @@ const Collection = sequelize.define(
name: DataTypes.STRING,
description: DataTypes.STRING,
color: DataTypes.STRING,
private: DataTypes.BOOLEAN,
type: {
type: DataTypes.STRING,
validate: { isIn: allowedCollectionTypes },
@@ -85,6 +87,11 @@ Collection.associate = models => {
foreignKey: 'collectionId',
onDelete: 'cascade',
});
Collection.belongsToMany(models.User, {
as: 'users',
through: models.CollectionUser,
foreignKey: 'collectionId',
});
Collection.belongsTo(models.User, {
as: 'user',
foreignKey: 'creatorId',
@@ -92,16 +99,20 @@ Collection.associate = models => {
Collection.belongsTo(models.Team, {
as: 'team',
});
Collection.addScope('withRecentDocuments', {
include: [
{
as: 'documents',
limit: 10,
model: models.Document,
order: [['updatedAt', 'DESC']],
},
],
});
Collection.addScope(
'defaultScope',
{
include: [
{
model: models.User,
as: 'users',
through: 'collection_users',
paranoid: false,
},
],
},
{ override: true }
);
};
Collection.addHook('afterDestroy', async model => {
@@ -112,8 +123,6 @@ Collection.addHook('afterDestroy', async model => {
});
});
// Hooks
Collection.addHook('afterCreate', model =>
events.add({ name: 'collections.create', model })
);
@@ -126,6 +135,22 @@ Collection.addHook('afterUpdate', model =>
events.add({ name: 'collections.update', model })
);
Collection.addHook('afterCreate', (model, options) => {
if (model.private) {
return CollectionUser.findOrCreate({
where: {
collectionId: model.id,
userId: model.creatorId,
},
defaults: {
permission: 'read_write',
createdById: model.creatorId,
},
transaction: options.transaction,
});
}
});
// Instance methods
Collection.prototype.addDocumentToStructure = async function(

View File

@@ -0,0 +1,34 @@
// @flow
import { DataTypes, sequelize } from '../sequelize';
const CollectionUser = sequelize.define(
'collection_user',
{
permission: {
type: DataTypes.STRING,
validate: {
isIn: [['read', 'read_write']],
},
},
},
{
timestamps: true,
}
);
CollectionUser.associate = models => {
CollectionUser.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'collectionId',
});
CollectionUser.belongsTo(models.User, {
as: 'user',
foreignKey: 'userId',
});
CollectionUser.belongsTo(models.User, {
as: 'createdBy',
foreignKey: 'createdById',
});
};
export default CollectionUser;

View File

@@ -220,7 +220,7 @@ Document.searchForUser = async (
ts_headline('english', "text", plainto_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"collectionId" IN(:collectionIds) AND
"deletedAt" IS NULL AND
("publishedAt" IS NOT NULL OR "createdById" = '${user.id}')
ORDER BY
@@ -230,20 +230,24 @@ Document.searchForUser = async (
OFFSET :offset;
`;
const collectionIds = await user.collectionIds();
const results = await sequelize.query(sql, {
type: sequelize.QueryTypes.SELECT,
replacements: {
query,
limit,
offset,
collectionIds,
},
});
// Second query to get associated document data
// Final query to get associated document data
const documents = await Document.scope({
method: ['withViews', user.id],
}).findAll({
where: { id: map(results, 'id') },
where: {
id: map(results, 'id'),
},
include: [
{ model: Collection, as: 'collection' },
{ model: User, as: 'createdBy', paranoid: false },

View File

@@ -6,7 +6,7 @@ import subMinutes from 'date-fns/sub_minutes';
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
import { sendEmail } from '../mailer';
import { Star, NotificationSetting, ApiKey } from '.';
import { Star, Collection, NotificationSetting, ApiKey } from '.';
const User = sequelize.define(
'user',
@@ -54,6 +54,25 @@ User.associate = models => {
};
// Instance methods
User.prototype.collectionIds = async function() {
let models = await Collection.findAll({
attributes: ['id', 'private'],
where: { teamId: this.teamId },
include: [
{
model: User,
through: 'collection_users',
as: 'users',
where: { id: this.id },
required: false,
},
],
});
// Filter collections that are private and don't have an association
return models.filter(c => !c.private || c.users.length).map(c => c.id);
};
User.prototype.updateActiveAt = function(ip) {
const fiveMinutesAgo = subMinutes(new Date(), 5);

View File

@@ -2,6 +2,7 @@
import ApiKey from './ApiKey';
import Authentication from './Authentication';
import Collection from './Collection';
import CollectionUser from './CollectionUser';
import Document from './Document';
import Event from './Event';
import Integration from './Integration';
@@ -18,6 +19,7 @@ const models = {
ApiKey,
Authentication,
Collection,
CollectionUser,
Document,
Event,
Integration,
@@ -42,6 +44,7 @@ export {
ApiKey,
Authentication,
Collection,
CollectionUser,
Document,
Event,
Integration,